Should semaphore wait and signal always be called from separate queues?

4.4k Views Asked by At

I was going through correct implementational details for semaphore using GCD, when one statement from (https://khanlou.com/2016/04/the-GCD-handbook/) confused me: "Calling .wait() will block the thread until .signal() is called. This means that .signal() must be called from a different thread, since the current thread is totally blocked. Further, you should never call .wait() from the main thread, only from background threads." Most of the examples of semaphore usually call wait and signal from the same queue and that seems to work fine too. Am I missing something here?

// Pseudocode from: https://khanlou.com/2016/04/the-GCD-handbook/
// on a background queue
let semaphore = DispatchSemaphore(value: 0)
doSomeExpensiveWorkAsynchronously(completionBlock: {
    semaphore.signal()
})
semaphore.wait()
//the expensive asynchronous work is now done
1

There are 1 best solutions below

3
On BEST ANSWER

You ask:

Should semaphore wait and signal always be called from separate queues?

Semaphores are always be called from separate threads. That’s the purpose of semaphores, for one thread to send a signal for which another thread will wait. That means that it’s safe to call semaphores from the same concurrent queue (because individually dispatched tasks run on different worker threads), but it’s not safe to call semaphores from the same serial queue. Obviously, it’s also safe to call semaphores from different queues. The main point is that it has to be different threads.

You shared a quote from that document, and everything the author said is absolutely correct. The wait and signal calls must be done from different threads. And we never want to wait on the main thread for some signal being sent by some other, time consuming, process.

You then went on to say:

Most of the examples of semaphore usually call wait and signal from the same queue and that seems to work fine too. Am I missing something here?

// Pseudocode from: https://khanlou.com/2016/04/the-GCD-handbook/
// on a background queue
let semaphore = DispatchSemaphore(value: 0)
doSomeExpensiveWorkAsynchronously(completionBlock: {
    semaphore.signal()
})
semaphore.wait()
//the expensive asynchronous work is now done

A few observations:

  1. This pattern only works if signal and wait are on separate threads. If they were the same thread, this would deadlock. So clearly the author is assuming that they’re on different threads.

  2. You seem to be implying that these two calls are “on the same queue”. That’s not a valid assumption (and, frankly, is quite unlikely). We would need to see the implementation of that “expensive asynchronously” method to be sure. But when you see a closure like this, it generally means that the method dispatched this closure to some GCD queue of its own choosing. And we have no way of knowing which it used. (You’d have to look at its implementation to be sure.) But it’s unlikely to be the same queue. And this code presumes that it must be a different thread.

  3. This whole pattern that you’ve shared with us here is ill-advised. It’s effectively taking an asynchronous method, using a semaphore to make it behave synchronously, but the code comment suggests that this whole thing was dispatched to a background queue (to avoid blocking the main thread) thereby making it asynchronous again. That’s a bit tortured. You really should just go ahead and call this expensive/asynchronous method from the main thread (which is safe, because it runs asynchronously) and lose the semaphore entirely. Maybe the author was contorting himself/herself to illustrate how one could use semaphores, but it’s a horrible example.