• A Quick Look at Semaphores in Swift 🚦
  • Federico Zanetello
  • The Nuggets translation Project
  • Translator: Deepmissea
  • Proofreader: Gocy015, Skyar2009

First, if you’re not familiar with GCDS and Dispatch Queues, check out this article from AppCoda.

All right! It’s time to talk about semaphores!

The introduction

Let’s imagine a group of writers sharing only one pen. Apparently, only one writer can use a pen at any given time.

Now, think of the writer as our thread, and the pen as our shared resource (which can be anything: a file, a variable, the right to do something, etc.).

How can we ensure that our resources are truly mutually exclusive?

Implement our own resource control access

One might think: I’ll just take a resourceIsAvailable variable of type Bool and set it to true or false to mutually exclusive.

if (resourceIsAvailable) {
  resourceIsAvailable = false
  useResource()
  resourceIsAvailable = true
} else {
  // resource is not available, wait or do something else
}Copy the code

The problem is that with concurrency, regardless of the priority between threads, there is no way to know for sure which thread will perform the next step.

example

Assuming we implement the code above, we have two threads, threadA and threadB, that use a mutually exclusive resource:

  • ThreadA reads the if condition and finds the resource available. Great!
  • However, before executing the next line of code (resourceIsAvalilable = false), the processor switches to threadB, which also reads the if condition.
  • Now both of our threads are sure that the resource is available, and they both execute the block that uses the resource part.

Writing thread-safe code without GCD is not an easy task.

How does a semaphore work

Three steps:

  1. When we need to use a shared resource, we send a request to its semaphore;
  2. Once the semaphore gives us the green light (see what I did here? , we can assume the resource is ours and use it;
  3. Once the resource is no longer needed, we let it know by sending a signal to the semaphore, which can then allocate the resource to another thread.

When there is only one resource available and only one thread is available at any given time, you can use these requests/signals as lock/unlock resources.

What’s going on behind the scenes

structure

The semaphore consists of the following two parts:

  • A counter that lets the semaphore know how many threads can use its resources;
  • A FIFO queue to keep track of the threads waiting for the resource;

To request resources: wait()

When a semaphore receives a request, it checks to see if its counter is greater than zero:

  • If so, the semaphore is decayed by one and the thread is given a green light;
  • If not, it adds the thread to the end of its queue;

Release resources: signal()

Once the semaphore receives a signal, it checks for threads in its FIFO queue:

  • If so, the semaphore pulls out the first thread and gives it a green light;
  • If not, it increments its counter;

Warning: Busy waiting

When a thread sends a wait() resource request to a semaphore, the thread freezes until the semaphore gives it the green light.

⚠️️ If you do this on the main thread, the entire application freezes ⚠️️

Using semaphores in Swift (via GCD)

Let’s write some code!

The statement

Declaring a semaphore is simple:

let semaphore = DispatchSemaphore(value: 1)Copy the code

The value parameter represents the number of threads allowed to access the resource simultaneously by the created semaphore.

The resource request

To request a semaphore resource, we simply need:

 semaphore.wait()Copy the code

Remember that the semaphore doesn’t actually give us anything, the resource is within the scope of the thread, and we just use the resource between the request and the release call.

Once the semaphore gives us the go-ahead, the thread will resume normal execution and can safely use the resource as its own.

Release resources

To free resources, we write:

semaphore.signal()Copy the code

After sending this signal, we can’t touch any resource until we request it again.

Semaphore in Playgrounds

Let’s look at semaphores in action by following the example of this article on AppCoda!

Note: These are Playground in Xcode, Swift Playground does not yet support logging. Hopefully WWDC17 will solve this problem!

In these playgrounds, we have two threads, one with a slightly higher priority than the others, printing 10 emojis and incremental numbers.

Playground without semaphore

import Foundation
import PlaygroundSupport

let higherPriority = DispatchQueue.global(qos: .userInitiated)
let lowerPriority = DispatchQueue.global(qos: .utility)

func asyncPrint(queue: DispatchQueue, symbol: String) {
  queue.async {
    for i in 0..10. {
      print(symbol, i)
    }
  }
}

asyncPrint(queue: higherPriority, symbol: "🔴")
asyncPrint(queue: lowerPriority, symbol: "🔵")

PlaygroundPage.current.needsIndefiniteExecution = trueCopy the code

As you might expect, most of the time, the high-priority line gets the job done first:

Playground with semaphore

This time we will use the same code as before, but at the same time, we will only give one thread the right to print emoticons + numbers.

To do this, we define a semaphore and update our asyncPrint function:

import Foundation
import PlaygroundSupport

let higherPriority = DispatchQueue.global(qos: .userInitiated)
let lowerPriority = DispatchQueue.global(qos: .utility)

let semaphore = DispatchSemaphore(value: 1)

func asyncPrint(queue: DispatchQueue, symbol: String) {
  queue.async {
    print("\(symbol) waiting")
    semaphore.wait()  // Request resources

    for i in 0..10. {
      print(symbol, i)
    }

    print("\(symbol) signal")
    semaphore.signal() // Release resources
  }
}

asyncPrint(queue: higherPriority, symbol: "🔴")
asyncPrint(queue: lowerPriority, symbol: "🔵")

PlaygroundPage.current.needsIndefiniteExecution = trueCopy the code

I also added some print instructions so that we can see the actual state of each thread’s execution.

As you can see, when one thread starts to print the queue, another thread must wait until the first thread finishes, and then the semaphore receives signal from the first thread. If and only if thereafter, the second thread can start printing its queue.

It doesn’t matter at which point in the queue the second thread sends wait(); it will wait until the other thread terminates.

Priority inversion

Now that we understand how each step works, take a look at this log:

In this case, with the code above, the processor decides to execute the low-priority thread first.

At this point, the higher priority thread must wait for the lower priority thread to complete! It’s true. It does happen. The problem is that even if a high-priority thread is waiting for it, a low-priority thread is a low-priority thread: this is called priority inversion.

In other programming concepts that differ from semaphores, when this happens, the lower-priority thread temporarily inherits the priority of the highest-priority thread waiting for it, which is called priority inheritance.

This is not the case when using semaphores; in fact, anyone can call signal() (not just the thread currently using the resource).

Thread starvation

To make things worse, let’s assume that there are 1,000 more middle-priority threads between our high-priority and low-priority threads.

If we have a priority inversion situation like the one above, the high-priority thread must wait for the low-priority thread, but, most of the time, the processor will execute the middle-priority thread because their priority is higher than our low-priority thread.

In this case, our high-priority threads are starving the CPU (hence the concept of starvation).

The solution

In my opinion, when using semaphores, it is best to use the same priority between threads. If this is not your case, I suggest you look at other solutions, such as critical blocks and pipes.

Deadlock on Playground

Now we have two threads, using two mutually exclusive resources, “A” and “B”.

If two resources can be used separately, it makes sense to define a semaphore for each resource; if not, a semaphore is sufficient to manage both.

I would like to use an example using the former case (2 resources, 2 semaphores) : A high-priority thread would use resource “A” first and then “B”, whereas A low-priority thread would use resource “B” first and then “A”.

Here’s the code:

import Foundation
import PlaygroundSupport

let higherPriority = DispatchQueue.global(qos: .userInitiated)
let lowerPriority = DispatchQueue.global(qos: .utility)

let semaphoreA = DispatchSemaphore(value: 1)
let semaphoreB = DispatchSemaphore(value: 1)

func asyncPrint(queue: DispatchQueue, symbol: String.firstResource: String.firstSemaphore: DispatchSemaphore, secondResource: String.secondSemaphore: DispatchSemaphore) {
  func requestResource(_ resource: String.with semaphore: DispatchSemaphore) {
    print("\(symbol) waiting resource \(resource)")
    semaphore.wait()  // requesting the resource
  }

  queue.async {
    requestResource(firstResource, with: firstSemaphore)
    for i in 0..10. {
      if i == 5 {
        requestResource(secondResource, with: secondSemaphore)
      }
      print(symbol, i)
    }

    print("\(symbol) releasing resources")
    firstSemaphore.signal() // releasing first resource
    secondSemaphore.signal() // releasing second resource
  }
}

asyncPrint(queue: higherPriority, symbol: "🔴".firstResource: "A".firstSemaphore: semaphoreA, secondResource: "B".secondSemaphore: semaphoreB)
asyncPrint(queue: lowerPriority, symbol: "🔵".firstResource: "B".firstSemaphore: semaphoreB, secondResource: "A".secondSemaphore: semaphoreA)

PlaygroundPage.current.needsIndefiniteExecution = trueCopy the code

If we’re lucky, it goes like this:

In simple terms, the first resource is made available to the higher priority thread, and the second resource can only be moved to the lower priority thread later.

However, if we’re not so lucky, this is what happens:

Neither thread has completed their execution! Let’s check the current state:

  • A high-priority thread is waiting for resource “B”, which is held by a low-priority thread.
  • The lower priority thread is waiting for resource “A”, which is held by the higher priority thread.

Both threads are waiting for each other’s resources, and neither can take a step forward: welcome to thread deadlocks!

The solution

Avoiding deadlocks is difficult. The best solution is to write code that doesn’t achieve this state to prevent them.

For example, on other operating systems, one of the deadlocked threads may be killed (to free up all its resources) in order for the other threads to continue executing.

. Or you can use Ostrich_Algorithm 😆.

conclusion

Semaphores are a great concept that can be easily used in many applications, just be careful: look both ways when crossing the street.


FedericoI am a software engineer based in Bangkok with a passion for Swift, Minimalism, Design and iOS development.