Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is this safe to call wait() of DispatchSemaphore several times at one time?

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")

        }
    }
}
like image 763
JsW Avatar asked Mar 07 '18 09:03

JsW


People also ask

How do you use Dispatchsemaphore?

A semaphore can be used in any case where you have a resource that can be accessed by at most N threads at the same time. You set the semaphore's initial value to N and then the first N threads that wait on it are not blocked but the next thread has to wait until one of the first N threads has signaled the semaphore.

What does semaphore wait do?

Semaphores are integer variables that are used to solve the critical section problem by using two atomic operations, wait and signal that are used for process synchronization. The wait operation decrements the value of its argument S, if it is positive. If S is negative or zero, then no operation is performed.


2 Answers

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

like image 182
JeremyP Avatar answered Oct 27 '22 04:10

JeremyP


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.

like image 31
Rob Avatar answered Oct 27 '22 03:10

Rob