DispatchSemaphore is an object that controls access to a resource across multiple execution contexts through use of a traditional counting semaphore.
Modifiying shared resources from multiple threads
Given the following setup code
let q1 = DispatchQueue(label: "q1", attributes: .concurrent)
let q2 = DispatchQueue(label: "q2", attributes: .concurrent)
var count = 0
func increment(queue: DispatchQueue) {
count = count + 1
print("write count: \(count) in queue: \(queue.label)")
}
func read(queue: DispatchQueue) {
print("read count: \(count) in queue: \(queue.label)")
}
Now we will try to read and write the count
variable from multiple threads
func perform(queue: DispatchQueue) {
increment(queue: queue)
read(queue: queue)
}
for _ in 1...5 {
q1.async {
perform(queue: q1)
}
q2.async {
perform(queue: q2)
}
}
The result you can see in the Xcode console would be
write count: 1 in queue: q1
read count: 1 in queue: q1
write count: 2 in queue: q2
write count: 2 in queue: q2
write count: 2 in queue: q1
read count: 2 in queue: q2
read count: 2 in queue: q2
write count: 2 in queue: q1
read count: 2 in queue: q1
write count: 3 in queue: q1
read count: 3 in queue: q1
read count: 3 in queue: q1
write count: 3 in queue: q2
write count: 4 in queue: q1
read count: 4 in queue: q2
read count: 4 in queue: q1
write count: 3 in queue: q2
read count: 3 in queue: q2
write count: 4 in queue: q2
read count: 4 in queue: q2
We can see that the count
variable is concurrently accessed and modified by 2 queues (threads). One queue might modify the value while the other queue will read an old, outdated value. This is an example of a race condition where multiple queues/threads trying to access and modify the same shared resources without synchronisation
DispatchSemaphore to rescue
// Semaphore is created using value 1. Value 0 will block all the threads to access the shared resource. value 1 will allow 1 thread at a time.
let semaphore = DispatchSemaphore(value: 1)
func perform(queue: DispatchQueue) {
// Increments semaphore count. if the value provided to semaphore equals the semaphore count. semaphore stop any more thread to access the critical section.
semaphore.wait()
increment(queue: queue)
read(queue: queue)
// Decrement semaphore count. Hence threads can again be allowed to access the critical section.
semaphore.signal()
}
for _ in 1...5 {
q1.async {
perform(queue: q1)
}
q2.async {
perform(queue: q2)
}
The output of the above modified script would be
write count: 1 in queue: q1
read count: 1 in queue: q1
write count: 2 in queue: q2
read count: 2 in queue: q2
write count: 3 in queue: q1
read count: 3 in queue: q1
write count: 4 in queue: q2
read count: 4 in queue: q2
write count: 5 in queue: q1
read count: 5 in queue: q1
write count: 6 in queue: q2
read count: 6 in queue: q2
write count: 7 in queue: q1
read count: 7 in queue: q1
write count: 8 in queue: q2
read count: 8 in queue: q2
write count: 9 in queue: q1
read count: 9 in queue: q1
write count: 10 in queue: q2
read count: 10 in queue: q2
The count
is now correctly increased by both queues 1 and 2, each queue will increase the count
variable 5 times, so the final output 10
is correctly printed out.
The access from 2 queues q1
and q2
to the shared variable count
is now synchronised by the semaphore
. At a given time, only one queue can access and modify the count
variable. The other queue must wait until the semaphore
release the shared resource for accessing.