I am facing an issue in using semaphores on iOS.
I am implementing a feature to execute a series of async methods sequentially, one after another in order.
let semaphore = DispatchSemaphore(value: 1)
semaphore.wait()
performFirstTask {
semaphore.signal
}
semaphore.wait()
performSecondTask {
semaphore.signal
}
semaphore.wait()
performThirdTask {
semaphore.signal
}
So this is working as expected, but the issue comes if the user moves away from the screen in the wait state, so when the callback from a particular task fires, the view might have deallocated, which is causing a crash,
Can anyone please help me to resolve this issue, I am not seeing any way to release the semaphores.
Thanks in advance
2
Answers
This semaphore-based code should be retired. Nowadays we would use the
async
–await
of Swift concurrency. See WWDC 2021 video Meet async/await in Swift, as well as the other videos referenced on that page.If you were not considering Swift concurrency for some reason (i.e., you need to support OS versions that don’t support
async
–await
), you might consider Combine, or custom asynchronousOperation
subclass, or a number of third party solutions (e.g., promises or futures). But nowadays, semaphores are an anti-pattern.Using semaphores has a number of problems:
That having been said, the problem is likely that your semaphore is deallocated when it has a value less than it was when it was created (e.g., you created it with a value of
1
and may have been0
when it was deallocated). See https://stackoverflow.com/a/70458886/1271826.You can avoid this problem by starting with a value of zero. To do this, you either need to:
Remove the first
wait
:Or if you need that first
wait
, just do a preemptivesignal
:Again, you should retire the use of semaphores entirely, but if you must, you can use either of the two techniques to make sure that the count when it is deallocated is not less than it was when it was initialized.
Let us imagine that you decided to adopt Swift concurrency, rather than using semaphores. So, what would this look like with
async
–await
?Let us imagine for a second that you refactored
performFirstTask
,performSecondTask
, andperformThirdTask
to adopt Swift concurrency. Then, you eliminate the semaphore completely, and your fifteen lines of code are reduced to:That performs those three asynchronous tasks sequentially, but avoids all of the downsides of semaphores. The whole idea of
async
–await
is that you can represent dependencies between a series of asynchronous tasks very elegantly.Now, generally you would refactor the
performXXXTask
methods to adopt Swift concurrency. Alternatively, you could also just writeasync
“wrapper” functions for them, e.g.:That is an
async
rendition ofperformFirstTask
that calls the completion handler rendition.However you decide to do it (refactor these three methods or just write wrappers for them), Swift concurrency simplifies the process greatly. See WWDC 2021 video Swift concurrency: Update a sample app for more examples about how to convert legacy code to adopt Swift concurrency.