8

I got three dispatched threads named queueA, queueB, queueC.
Now I want the queueA executed after queueB and queueC done.
So I tried to implement it by DispatchSemaphore.
My Problem is:
Is this safe to call wait() two times in a thread at one time to make the semaphore 2?

 self.semaphore.wait()  // -1
 self.semaphore.wait()  // -1

The following is the entire test code:

class GCDLockTest {
    let semaphore = DispatchSemaphore(value: 0) 
   
    func test() {
        
        let queueA = DispatchQueue(label: "Q1")
        let queueB = DispatchQueue(label: "Q2")
        let queueC = DispatchQueue(label: "Q3")
        queueA.async {
            self.semaphore.wait()  // -1
            self.semaphore.wait()  // -1
            print("QueueA gonna sleep")
            sleep(3)
            print("QueueA woke up")                
        }
        queueB.async {
            self.semaphore.signal()  // +1
            print("QueueB gonna sleep")
            sleep(3)
            print("QueueB woke up")

        }
        queueC.async {
            self.semaphore.signal()  // +1
            print("QueueC gonna sleep")
            sleep(3)
            print("QueueC wake up")

        }
    }
}
JsW
  • 1,682
  • 3
  • 22
  • 34

2 Answers2

12

First of all, on a slightly pedantic note, it is signal that increments the semaphore and wait that decrements it (unless it is zero, in which case it waits).

Is this safe to call wait() two times in a thread at one time to make the semaphore 2 [sic]?

Semaphore actions are guaranteed to be thread safe, there would be no point in having them if not, so what you are doing will work fine. You can indeed call wait twice to conceptually grab a resource twice.

However, you have a background thread that is blocking. This is a bad thing because threads used to execute blocks on dispatch queues aren't created when they are needed, they are allocated from a pool that is sized based on various things like number of processor cores. Your block on queue A will tie up a thread until the queue B and queue C threads both signal the semaphore.

The worst case scenario occurs when you enter function test() with only one thread remaining in the thread pool. If the block on queue A grabs it before either of the other two blocks, you will have a dead lock because A will be waiting on a semaphore and B and C will be waiting on A to finish so they can have a thread.

It would be better to not start A until the other two threads are ready to kick it off. This can be done by executing a block on the main thread at the right time. Something like this:

class GCDLockTest {
    var cFinished = false
    var bFinished = false 

    func test() {

        let queueA = DispatchQueue(label: "Q1")
        let queueB = DispatchQueue(label: "Q2")
        let queueC = DispatchQueue(label: "Q3")
        queueB.async {
             DispatchQueue.main.async
             {
                 bFinished = true
                 if cFinished
                 {
                     queueA.async {
                         print("QueueA gonna sleep")
                         sleep(3)
                         print("QueueA woke up")                
                     }
                 }
             }
            print("QueueB gonna sleep")
            sleep(3)
            print("QueueB woke up")

        }
        queueC.async {
             DispatchQueue.main.async
             {
                 cFinished = true
                 if bFinished
                 {
                     queueA.async {
                         print("QueueA gonna sleep")
                         sleep(3)
                         print("QueueA woke up")                
                     }
                 }
             }
            print("QueueC gonna sleep")
            sleep(3)
            print("QueueC wake up")

        }
    }
}

In the above, you don't need any semaphores or other synchronisation because it is implicit in that all the synchronisation work is done on the main queue which is serial. i.e. the two blocks that start A can never be running at the same time.

That's one way to do it, but Apple provides dispatch groups for exactly your problem. With dispatch groups you can add B and C to a group and have them tell the group when they are ready for A to start.

class GCDLockTest {

    func test() {

        let group = DispatchGroup()

        let queueA = DispatchQueue(label: "Q1")
        let queueB = DispatchQueue(label: "Q2")
        let queueC = DispatchQueue(label: "Q3")
        group.enter()
        queueB.async {
            group.leave()
            print("QueueB gonna sleep")
            sleep(3)
            print("QueueB woke up")

        }
        group.enter()
        queueC.async {
            group.leave()
            print("QueueC gonna sleep")
            sleep(3)
            print("QueueC wake up")

        }
        group.notify(queue: queueA) {
            print("QueueA gonna sleep")
            sleep(3)
            print("QueueA woke up")                
    }
}

Before starting each of B and C the group is entered. Then after starting B and C, we place a notify block in the group so that when they both leave the group, the block for A is started on the right queue.

See also https://developer.apple.com/documentation/dispatch/dispatchgroup

JeremyP
  • 84,577
  • 15
  • 123
  • 161
  • Do you have a good reference for the GCD thread pool being bounded at some number less than the OS thread limit? GCD tries to remove user concern over threads and provides semaphores, to argue you should avoid the latter because you should be concerned about the former seems to challenge the GCD design. That's not to say using a semaphore (maybe with a different initial value - nobody seems to have mentioned that yet) is a better solution than using thread groups as you suggest. – CRD Mar 07 '18 at 16:50
  • But I got confused about one thing. When I added `wait()` in `DispatchQueue.main.async`, it will block the UI refreshing. It supposed to only block a thread in the main queue, which should not cause the UI to stop refreshing. I mean did I have any misunderstanding in semaphore's mechanism. Thank you for your help. It helps me a lot. – JsW Mar 08 '18 at 07:04
  • 1
    @JsW - the GCD main queue is serial and runs its blocks on the main thread, which explains the behaviour you see. Do not block the main thread. – CRD Mar 08 '18 at 08:54
  • @CRD No wander that! I thought all the default dispatched queues including main, global and those queues created without explicit declare are concurrent. – JsW Mar 08 '18 at 09:11
  • @CRD What's the difference of `async` and `sync` of the serial queue? It seems both of those tasks are executed orderly on by one. – JsW Mar 08 '18 at 09:30
  • @CRD I do not argue you should avoid semaphores. I argue that you should avoid blocking background dispatch tasks where possible. Even if there were no [hard thread limit](https://stackoverflow.com/questions/15150308/workaround-on-the-threads-limit-in-grand-central-dispatch) a task waiting on a semaphore is using system resources to do nothing. – JeremyP Mar 08 '18 at 09:38
  • @JeremyP - An internet search can quickly turn up statements that GCD has a hard thread limit below the OS limit, with *different* numbers quoted for that limit, and statements that the thread pool is expanded - hence my asking whether you had a "good reference". We have the source GCD of course... – CRD Mar 08 '18 at 10:36
  • @JeremyP - On the other point, while you state you "do not argue you should avoid semaphores" you immediately follow that with "waiting on a semaphore is using system resources to do nothing" which sounds rather like an argument against them... Yet Apple choose to provide them in GCD for some reason... I just read your answer as being somewhat overly emphatic against semaphores without accompanying justication. – CRD Mar 08 '18 at 10:41
  • 1
    @JsW - an async request to a serial queue means the requester continues immediately without waiting for the request to complete (or even start) - the main thread/queue will process the queue requests in order when it can (in an event Drive app the queue is examined once per iteration of the run loop). A sync request will cause the requester to block until the main thread/queue gets to and completes the request. I.e. async and sync mean the same regardless whether a queue is serial or concurrent. [cont...] – CRD Mar 08 '18 at 10:52
  • 1
    [...cont] If a queued request, sync or async, blocks then on a serial queue it blocks the whole queue while a concurrent queue can carry on running requests on other threads (if any are available). So a sync request blocks the requester, and a blocking request on a serial queue blocks the queue. Hope that makes sense! – CRD Mar 08 '18 at 10:55
  • @CRD What does it matter if the hard limit varies from device to device and version of GCD to version of GCD. The limit exists. People have hit it (as evidenced by the question I linked). – JeremyP Mar 08 '18 at 10:59
  • @CRD "...which sounds rather like an argument against them". That is good to know. It *is* an argument against using them *in this use-case*. If you have a choice between grabbing a thread and immediately suspending it for a bit until you are ready or only grabbing the thread when you are ready, what would *you* do? – JeremyP Mar 08 '18 at 11:06
  • @JeremyP - Apologies but my intended tone in the comment has been misunderstood, mea culpa. Maybe just note that I choose not to add an answer with more "accompanying justification", covered why you don't need two waits, etc. but rather left a comment to give you the opportunity to do so if you wished. Have a nice day (any more back and forth and SO will send us both to stand in the corner ;-)). – CRD Mar 08 '18 at 12:03
  • 1
    Question: The OP mentions queueA start executing *after* B and C are done. So the example in the post notwithstanding, shouldn't `group.leave()` be called **after** `sleep(3)`? Or does it not matter? – funct7 Dec 08 '19 at 12:00
7

A few thoughts:

  1. Yes, you can use multiple wait calls without problem. You need one wait for each signal.

  2. If you were to do that, you presumably want to put your signal calls at the end of those dependent closures, not at the start of them. Right now you're waiting for those two tasks to start, not waiting for them to finish.

  3. As mentioned elsewhere, a much better mechanism is dispatch groups. But you don't need manual enter/leave calls. You can just use group parameter to async

    class GCDLockTest {
        func test() {
            let group = DispatchGroup()
    
            let queueA = DispatchQueue(label: "Q1")
            let queueB = DispatchQueue(label: "Q2")
            let queueC = DispatchQueue(label: "Q3")
    
            queueB.async(group: group) {
                print("QueueB gonna sleep")
                sleep(3)
                print("QueueB woke up")
            }
    
            queueC.async(group: group) {
                print("QueueC gonna sleep")
                sleep(3)
                print("QueueC wake up")
            }
    
            group.notify(queue: queueA) {
                print("QueueA gonna sleep")
                sleep(3)
                print("QueueA woke up")                
            }
        }
    }
    

    You only need manual enter/leave calls if you're dispatching something that is, itself, asynchronous and has, for example, its own completion handler. But for an example like this, async(group:) is easiest.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Yes, thanks. Actually, my propose to ask this question is whether it's safe to continuously call two `wait()`. That demo is just a, maybe bad, scenario. – JsW Mar 15 '18 at 06:03
  • Like I said, yes you can, but it's not a good design to tie up a thread if you don't have to. And if you're doing it "continuously", that would make me even more apprehensive. – Rob Mar 15 '18 at 06:06
  • 1
    Yes, I know. I was not sure I got the right understanding about the mechanism of `wait()` and `signal()`. I know it now, like the adopted answer said, it's ok but not recommended. – JsW Mar 15 '18 at 06:23
  • What if there are five queues, C is executed after A and B, D after C, E after D and they are not in one same synchronous thread? – JsW Mar 15 '18 at 06:25
  • Thanks, buddy. That's just another bad scenario, please forget it. – JsW Mar 15 '18 at 09:42
  • @Rob I have a question. Most examples using `DispatchGroup` has called `enter()` before calling `async` on other queues, while the documentation says "Explicitly indicates that a block **has** entered the group", not **will** enter. I don't think it really matters since the submitted tasks won't execute in that cycle anyways, but does it matter? Or is it some idiomatic way of doing things to remind the programmer that the following block is associated with the `group.enter()`? – funct7 Dec 08 '19 at 12:04
  • Please let me know if there are any errors in my comment. I'm learning a lot from your answers btw. – funct7 Dec 08 '19 at 12:05
  • 1
    @funct7 - A group is merely a counter that is incremented on `enter()`, decremented on `leave()`, and calls the `notify()` block when the counter hits zero. So, “**has** entered” is technically correct. Re `async(group:execute:)`, this is just a more robust and more concise pattern saving you from manually calling `enter()` and `leave()`, but it only works if you're calling `async` yourself. But often we're dealing with a completion handler closure and are therefore forced to manually call `enter()` and `leave()` ourselves. But in this case, since we can use `async(group:execute:)`, we should. – Rob Dec 08 '19 at 16:52