3

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
coderex
  • 61
  • 1
  • 8
  • Presumably that completion block *doesn't* run on the same queue, otherwise this wouldn't work at all. Further, waiting on asynchronous work makes it synchronous. If you're waiting for it, it ain't async. That defeats the entire purpose of it being async in the first place. – Alexander May 27 '20 at 21:05
  • 2
    It is also worthwhile noting that threads and queues are different things. Queues dispatch work onto an available thread but except in the case of the main queue/main thread there is no binding between a queue and a specific thread. – Paulw11 May 27 '20 at 21:16
  • If you want to force an asynchronous task to become synchronous don’t do that. Learn to understand how asynchronous data processing works. – vadian May 27 '20 at 21:22
  • Basically if you're using a semaphore at all you're doing it wrong. It's better to think about real life and real tasks and not bang around with edge cases just for the sake of banging around. – matt May 27 '20 at 21:27
  • 1
    Don't mistake *queues* and *threads*. One queue can execute work items on multiple threads if it's a concurrent queue. If you call `.wait` on a thread, expecting a future `.signal` call from the same thread, you're gonna be sad (which is to say, blocked forever). Also, just don't call `.wait`. Use completion blocks, etc. Dispatch groups can be a useful tool for situations like this. – ipmcc May 27 '20 at 21:57

1 Answers1

5

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.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • I think the example used from the mentioned article seems to raise a lot unintentional questions. @Rob thanks for the detailed explanation. I actually misunderstood threads with queues I would like to jot down few points as per my understanding from your explanation: 1. Semaphores themselves manage separate threads to call wait and signal, IFF called on a concurrent queue. 2. Serial queues dispatch all of their work on a single thread. i would still like to understand is it a good practice to use semaphore to add dependencies between multiple async task or is there any better way? – coderex Jun 08 '20 at 09:50
  • 1
    “Is it good practice to use semaphore to add dependencies between multiple async tasks?” - Because semaphores are susceptible to deadlocks when misused and because calling `wait` blocks threads (which are fairly limited) semaphores are often the last tool we’ll reach for. Depending upon the application, dispatch groups (with `notify`, not `wait`) is often preferable. Or use custom `Operation` subclass. Or, now, with Combine. But we can’t answer this in the abstract. You have to provide concrete example. But the example you gave in your question is a textbook example of poor use of semaphores. – Rob Jun 08 '20 at 15:39
  • 1
    @coderex - 1. Yes, semaphores are used to communicate between different threads, whether those threads are from completely different queues or different worker threads used by the same concurrent queue (but not for serial queues). 2. Serial queues dispatch all of their work to run on only one thread at a time. Admittedly, on a custom serial queue, that may be a different worker from one call to another, but as only one can possibly run at a given time, the warning about not using semaphore for a serial queue to wait for a signal from itself still stands. – Rob Jun 08 '20 at 15:49