The Beauty of Semaphores in Swift 🚦

Roy Kronenfeld
6 min readMar 23, 2018

In this story, we will do the following:

  • Understand what Semaphores are
  • Understand how Semaphores work
  • Implement and explain 2 examples

Let’s Start

Semaphores give us the ability to control access to a shared resource by multiple threads. For an easy start, let’s consider the following real-life scenario:

A father sits with his three kids at home, then he pulls out an iPad…

Kid 2: I want to play with the iPad!!!
Kid 1: NO!, I want to play first…
Kid 3: Ipad! Ipad! Ipad! *sound of claps*
Father: Ok, Kid 2, since you asked first and no one is currently using the iPad, take it, but let me know once you are done. Rest of kids, please wait patiently.
Kid 2: (5 min later) I’m done father.
Father: Kid 1, the iPad is available, let me know once you are done.
Kid 1: (5 min later) I’m done father.
Father: Kid 3, the iPad is available, let me know once you are done.
Kid 3: (5 min later) I’m done father.

In the scenario above, the father is the semaphore, the iPad is the shared resource, and the kids are the threads. Note how the father makes sure that only one kid uses the iPad at a time. If we compare this to programming, only one thread has access to a shared resource at a time. In addition, note the order of use, the first who asked is the first who get (FIFO).

Tip: a shared resource can represent a variable, or a job such as downloading an image from a URL, reading from a database, etc.

What if the father just gave the iPad to the kids? A fight would build up to the point of a probably broken iPad 😖. If we compare this to programming, multiple threads try to access the same resource at the same time and nothing is preventing it. Such behavior could lead to race conditions, crashes, and obviously, our code won’t be thread-safe.

Thread-safe: code that can be safely called from multiple threads without causing any issues.

A Bit of Theory

A semaphore consists of a threads queue and a counter value (type Int).

The threads queue is used by the semaphore to keep track of waiting threads in FIFO order (The first thread entered into the queue will be the first to get access to the shared resource once it is available).

The counter value is used by the semaphore to decide if a thread should get access to a shared resource or not. The counter value changes when we call signal() or wait() function.

So, when should we call wait() and signal() functions?

  • Call wait() each time before using the shared resource. We are basically asking the semaphore if the shared resource is available or not. If not, we will wait.
  • Call signal() each time after using the shared resource. We are basically signaling the semaphore that we are done interacting with the shared resource.

Calling wait() will do the following:

  • Decrement semaphore counter by 1.
  • If the resulting value is less than zero, the thread is frozen.
  • If the resulting value is equal to or bigger than zero, the code will get executed without waiting.

Calling signal() will do the following:

  • Increment semaphore counter by 1.
  • If the previous value was less than zero, this function wakes the oldest thread currently waiting in the thread queue.
  • If the previous value is equal to or bigger than zero, it means the thread queue is empty, aka, no one is waiting.

Flow chart

Hmmm… I know, I know, this is getting confusing, no worries, let’s jump into the code and things will get clearer.

Example 1

In order to be consistent, this example will be based on the kids-iPad story.

First, let’s create a semaphore instance:

let semaphore = DispatchSemaphore(value: 1)

DispatchSemaphore init function has one parameter called “value”. This is the counter value that represents the number of threads we want to allow access to a shared resource at a given moment. In this case, we want to allow only one thread (kid) to access the shared resource (iPad), so let’s set it to 1.

Next, let’s create 3 global queues, each one represent a kid. Each kid will do the following: wait() → play iPad → signal()

DispatchQueue.global().async {
print("Kid 1 - wait")
semaphore.wait()
print("Kid 1 - wait finished")
sleep(1) // Kid 1 playing with iPad
semaphore.signal()
print("Kid 1 - done with iPad")
}
DispatchQueue.global().async {
print("Kid 2 - wait")
semaphore.wait()
print("Kid 2 - wait finished")
sleep(1) // Kid 1 playing with iPad
semaphore.signal()
print("Kid 2 - done with iPad")
}
DispatchQueue.global().async {
print("Kid 3 - wait")
semaphore.wait()
print("Kid 3 - wait finished")
sleep(1) // Kid 1 playing with iPad
semaphore.signal()
print("Kid 3 - done with iPad")
}

Console:

As we can see, all three kids start with wait(). Since kid 1 was the first, at that point the shared resource (iPad) was available. Once kid 1 finished playing, kid 2 got awaken and began playing, and so on.

Let’s track the semaphore counter for a better understanding:

  • 1 (our initial value)
  • 0 (kid 1 wait, since value >= 0, kid 1 can play the iPad)
  • -1 (kid 2 wait, since value < 0, it enters threads queue)
  • -2 (kid 3 wait, since value < 0, it enters thread queue)
  • -1 (kid 1 signal, last value < 0, wake up kid 2 and pop it from queue)
  • 0 (kid 2 signal, last value < 0, wake up kid 3 and pop it from queue)
  • 1 (kid 3 signal, last value >= 0, no threads are waiting to be awaken)

Example 2

Now that we understand how semaphores work, let’s go over a scenario that is more realistic for an app, and it is, downloading 15 songs from a URL.

First, we create a concurrent queue that will be used for executing our song downloading blocks of code.

Second, we create a semaphore and we set it with an initial counter value of 3, can you guess why? 🤭 well, we decided to download 3 songs at a time in order not to take too much CPU time at once.

Third, we iterate 15 times using a for a loop. On each iteration we do the following: wait() → download song → signal()

let queue = DispatchQueue(label: "com.gcd.myQueue", attributes: .concurrent)
let semaphore = DispatchSemaphore(value: 3)
for i in 0 ..> 15 {
queue.async {
let songNumber = i + 1
semaphore.wait()
print("Downloading song", songNumber)
sleep(2) // Download take ~2 sec each
print("Downloaded song", songNumber)
semaphore.signal()
}
}

Console:

Let’s track the semaphore counter for a better understanding:

  • 3 (our initial value)
  • 2 (song 1 wait, since value >= 0, start song download)
  • 1 (song 2 wait, since value >= 0, start song download)
  • 0 (song 3 wait, since value >= 0, start song download)
  • -1 (song 4 wait, since value < 0, add to queue)
  • -2 (song 5 wait, since value < 0, add to queue)
  • Repeats for all songs, it will take us to counter value of -12
  • -12 (song 15 wait, sing value < 0, add to queue)
  • -11 (song 1 signal, since last value < 0, wake first song in queue)
  • -10 (song 2 signal, since last value < 0, wake first song in queue)
  • You can continue this yourself in order to be sure you got the idea…

Tips

  • 🚧 NEVER run semaphore wait() function on the main thread as it will freeze your app.
  • Wait() function allows us to specify a timeout. Once timeout is reached, the wait will finish regardless of semaphore count value.

Conclusion

If you got here, it means you survived my tutorial, well done! I know it was a bit long, it is important not to only understand what semaphores are, but also how they work, and how to work with them. Using semaphores we can be sure in 100% that a shared resource will be used by ONLY 1 (or more) threads at a given moment — LOVELY.

signal() 😉

--

--