iOS Interview - DispatchSemaphore explained

March 18, 20244 min read#swift, #concurrency, #ios, #interview

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.


Profile picture

Personal blog by An Tran. I'm focusing on creating useful apps.
#Swift #Kotlin #Mobile #MachineLearning #Minimalist


© An Tran - 2024