Semaphore in iOS Swift
Asked Answered
I

2

5

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

Intermit answered 29/8, 2022 at 17:11 Comment(9)
Don't use semaphores.Scylla
use OperationQueue instead. Or async/await if it fitsMothball
I tried using operationqueue , but it seems to execute in parallel, I am not sure how to initiate the second task after the completion of the first one.. Can you please help me with a sampleIntermit
You have to set the max concurrency to 1.Scylla
Or why not just use a DispatchQueue, they are serial by default (when you create them from scratch).Scylla
DispatchQueue also tried , but the issue is I want the second task to start only after the completion of first one , and in that orderIntermit
Yep, that's what a serial DispatchQueue does. This has "xy question" written all over it.Scylla
Switch to async await, this stuff is over complicated and prone to bugs developer.apple.com/wwdc21/10132Pittel
@Scylla - FWIW, I did try to get him to volunteer more information about the underlying problem in his last question, but he seemed reluctant to go there.Lithuanian
L
6

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, and performThirdTask to adopt Swift concurrency. Then, you eliminate the semaphore completely, and your fifteen lines of code are reduced to:

Task {
    await performFirstTask()
    await performSecondTask()
    await performThirdTask()
}

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 write async “wrapper” functions for them, e.g.:

func performFirstTask() async {
    await withCheckedContinuation { continuation in
        performFirstTask() {
            continuation.resume(returning: ())
        }
    }
}

That is an async rendition of performFirstTask 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.

Lithuanian answered 29/8, 2022 at 23:22 Comment(3)
Thanks for the answer. Just wondering how should I do if performSecondTask() could be called by external objects, not at a certain timing, such as always following performFirstTask. And I still want task2 to wait till task1 finishesAgeratum
You could use Task object to keep track of first task, that way independent calls to perform second and third tasks could await the result of this first one. E.g., gist.github.com/robertmryan/bc5712e8c185848962eafe07d5137fa8. If you still have questions about that, rather then posting them in comments here, please either post them under that gist, or post your own Stack Overflow question.Lithuanian
the gist is awesome, thank you very much for your prompt and detailed replyAgeratum
L
9

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 asynchronous Operation 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:

  • It is inefficient (as it unnecessarily ties up a thread);
  • It introduces deadlock risks if not careful;
  • It can result in substandard UX and/or watchdog process killing your app if you do this on the main thread.

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 been 0 when it was deallocated). See https://mcmap.net/q/943082/-safe-to-signal-semaphore-before-deinitialization-just-in-case.

You can avoid this problem by starting with a value of zero. To do this, you either need to:

  1. Remove the first wait:

    let semaphore = DispatchSemaphore(value: 0)   // not 1
    
    // semaphore.wait()
    performFirstTask {
       semaphore.signal()
    }
    
    semaphore.wait()
    performSecondTask {
       semaphore.signal()
    }
    
    …
    
  2. Or if you need that first wait, just do a preemptive signal:

    let semaphore = DispatchSemaphore(value: 0)    // not 1
    semaphore.signal()                             // now bump it up to 1
    
    semaphore.wait()
    performFirstTask {
       semaphore.signal()
    }
    
    semaphore.wait()
    performSecondTask {
       semaphore.signal()
    }
    
    …
    

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.

Lithuanian answered 29/8, 2022 at 19:51 Comment(0)
L
6

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, and performThirdTask to adopt Swift concurrency. Then, you eliminate the semaphore completely, and your fifteen lines of code are reduced to:

Task {
    await performFirstTask()
    await performSecondTask()
    await performThirdTask()
}

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 write async “wrapper” functions for them, e.g.:

func performFirstTask() async {
    await withCheckedContinuation { continuation in
        performFirstTask() {
            continuation.resume(returning: ())
        }
    }
}

That is an async rendition of performFirstTask 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.

Lithuanian answered 29/8, 2022 at 23:22 Comment(3)
Thanks for the answer. Just wondering how should I do if performSecondTask() could be called by external objects, not at a certain timing, such as always following performFirstTask. And I still want task2 to wait till task1 finishesAgeratum
You could use Task object to keep track of first task, that way independent calls to perform second and third tasks could await the result of this first one. E.g., gist.github.com/robertmryan/bc5712e8c185848962eafe07d5137fa8. If you still have questions about that, rather then posting them in comments here, please either post them under that gist, or post your own Stack Overflow question.Lithuanian
the gist is awesome, thank you very much for your prompt and detailed replyAgeratum

© 2022 - 2024 — McMap. All rights reserved.