DEV Community

Cover image for Using semaphores in iOS to serialise callback completion
Tarik Dahic
Tarik Dahic

Posted on • Originally published at tarikdahic.com on

Using semaphores in iOS to serialise callback completion

In most of the situations when you are developing apps you will encounter APIs provided to you by someone else (framework, library, system, etc.). You cannot change the API and you need to use it as it is. Let’s say, to use that API, you give it a function that will be called when the result is ready or some action is completed (also known as a callback).

But, what if you don’t want to continue if the result is not ready or action is not completed? I’ve found that using semaphores is good for this situation and I will describe how to do it in this article. But first, we need to learn more about semaphores.

Semaphore 🚦

Semaphore mechanism was proposed by Dijkstra in the sixties which is a very significant technique to manage concurrent operations on shared resources. A trivial semaphore is a variable, like integer or a boolean that is incremented/decremented or toggled and depending on the state of that variable threads can access the resource or wait for it to become free.

A useful way to think of a semaphore as used in a real-world system is as a record of how many units of a particular resource are available, coupled with operations to adjust that record safely (i.e., to avoid race conditions) as units are acquired or become free, and, if necessary, wait until a unit of the resource becomes available.

There are two types of semaphores:

  • Binary Semaphore — It can have only two values – 0 and 1, and we can also represent this semaphore with a boolean. It will allow only one thread to use the resource while others will wait.
  • Counting Semaphore — By creating this semaphore we provide it with a number of how many threads are allowed to use it at the same time. When threads use the resource the semaphore counter is decremented. If the counter reaches 0 the threads that want to access the resource will wait until one of the threads is finished.

If we think of the implementation, a simple semaphore could be implemented by using one integer variable and one FIFO queue. Initial count of the integer variable determines how many available resources we have for consumers to use. If someone asks for access we decrement the count variable and if the value is not negative we allow the access to the resource. If the value is negative we add that consumer to FIFO queue and halt it until one of the consumers that is already using the shared resource signals that it has finished. When the thread signals that it has finished using the shared resource we increment the count variable and if threads are waiting in the FIFO queue we use the first one and give it access to the shared resource. Implementation like this where there is no prioritisation of threads can lead to a problem called priority inversion.

The operation that consumers use to request access of the shared resource which decrements the count variable is called wait and the operation of incrementing the count variable and telling the semaphore that we’re done using the shared resource is called signal.

Another great analogy that I found is:

Semaphore is the number of free identical toilet keys. Example, say we have four toilets with identical locks and keys. The semaphore count — the count of keys — is set to 4 at beginning (all four toilets are free), then the count value is decremented as people are coming in. If all toilets are full, ie. there are no free keys left, the semaphore count is 0. Now, when eq. one person leaves the toilet, semaphore is increased to 1 (one free key), and given to the next person in the queue.

Officially: “A semaphore restricts the number of simultaneous users of a shared resource up to a maximum number. Threads can request access to the resource (decrementing the semaphore), and can signal that they have finished using the resource (incrementing the semaphore).”Ref: Symbian Developer Library

Semaphores in iOS

In iOS semaphore implementation is a part of the Dispatch framework. You can use semaphores with Obj-C and Swift. I will provide an example for both languages.

The API to use semaphores is not complicated and it is very straightforward. We have 3 operations: create, wait and signal. In semaphore implementation for iOS the wait operation can be requested with a timeout to limit how much we want to wait to resource to become free. I advise to always provide a timeout so that the threads don’t become blocked.

To create the semaphores we can use the code below:

// Swift
let semaphore = DispatchSemaphore(value: 2)

// Obj-C
dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
Enter fullscreen mode Exit fullscreen mode

Now, if we want to wait and signal we can use the operations below:

// Wait - Swift
semaphore.wait(timeout: .now() + 5)
// Wait - Obj-C
dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)));

// Signal - Swift
semaphore.signal()
// Signal - Obj-C
dispatch_semaphore_signal(semaphore);
Enter fullscreen mode Exit fullscreen mode

Serialising callback completion

While dealing with some async APIs that take completion handlers I’ve run in to a problem where I need to wait for the execution of that API call to complete so that I can continue with the execution of my program. I didn’t want to end the function that is currently running and I used semaphores to wait for the async API to complete.

This has proven very useful to me when I work on iOS App Extensions that run in the background and that have very limited time of execution. While dealing with UNUserNotificationCenter that provides async operations this technique helped me a lot. So, without further ado, let’s go to the example.

In the code below I have a function that takes another function as a parameter and runs it after the work is completed. This is just a simulation of what other real-world APIs provide.

func doSomethingAndNotifyMe(via completion: @escaping () -> Void) {
    DispatchQueue.global().async { // some work is being done here :)
        sleep(3)
        completion()
    }
}
Enter fullscreen mode Exit fullscreen mode

I will use doSomethingAndNotifyMe in the example below to see what happens and how my code executes:

print("1. Starting the work")

doSomethingAndNotifyMe {
    print("2. Work done")
}

print("3. End of the work")
Enter fullscreen mode Exit fullscreen mode

After running the example above, I get this output:

1. Starting the work
3. End of the work
2. Work done
Enter fullscreen mode Exit fullscreen mode

You can see that the output messages are not in the order I want them to be, they are ordered as 1, 3, 2 and I want the order to be 1, 2, 3. To have the order that I want I will introduce semaphore to the example above:

print("1. Starting the work")
let semaphore = DispatchSemaphore(value: 0)

doSomethingAndNotifyMe {
    print("2. Work done")
    semaphore.signal()
}

semaphore.wait(timeout: .now() + 5)
print("3. End of the work")
Enter fullscreen mode Exit fullscreen mode

When I run the example with the semaphore I get the output that I want:

1. Starting the work
2. Work done
3. End of the work
Enter fullscreen mode Exit fullscreen mode

You can see that I initialised the semaphore with the value 0. This is not a practice that is commonly seen and I like to think of this type of a semaphore as a blocking semaphore. You can think of it as using a lock where the default state of the lock is locked.

Wait operation can be dangerous so don’t use it on the main thread and always try to provide the timeout for it.

Although this might not be the best practice for this kind of situations it has really proven to work great. A modern alternative to using semaphores for examples like this is using DispatchGroup API from the Dispatch library. DispatchGroup is only available on Apple platforms while semaphore implementations are available for almost every platform.

Have you used semaphores in your projects or any other mechanisms for dealing with multi-threaded environments? Please share your experiences and thank you for reading this article.

Discussion (2)

Collapse
farhanaxmustafa profile image
Farhana

Great post Tarik! I really appreciated that you did both Swift and Objective C examples side by side

Collapse
daholino profile image
Tarik Dahic Author

Thank you Farhana!