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")
}
}
}
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.
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.
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
A few thoughts:
Yes, you can use multiple wait
calls without problem. You need one wait
for each signal
.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With