How to pause and resume a sequence of mutating Swift structs using dispatch semaphore?
What is a semaphore?
“Semaphores are an old-school threading concept introduced to the world by the ever-so-humble Edsger W. Dijkstra. Semaphores are a complex topic because they build upon the intricacies of operating system functions.
If you want to learn more about semaphores, check out this link which discusses semaphore theory in more detail. If you’re the academic type, a classic software development problem that uses semaphores is the Dining Philosophers Problem.
Semaphores let you control the access of multiple consumers into a finite amount of resources. For example, if you created a semaphore with a pool of two resources, at most only two threads could access the critical section at the same time. Other items that want to use the resource must wait in a…have you guessed it?… FIFO queue!” — Derek Selander
How do you use a semaphore?
In GCD, we can create a semaphore using dispatch_semaphore_create:
“Passing zero for the value is useful for when two threads need to reconcile the completion of a particular event. Passing a value greater than zero is useful for managing a finite pool of resources, where the pool size is equal to the value.” — Apple Docs
In the example above, the semaphore starts with a count of 0. When the semaphore count becomes a negative number it pauses the thread that it is being executed on.
To decrement the semaphore count by 1 we use dispatch_semaphore_wait, passing the semaphore and the time we want to wait for:
dispatch_semaphore_wait should NOT be called on the main thread.
To signal the semaphore to continue the background thread execution, we use dispatch_semaphore_signal which increments the semaphore count by 1:
Code execution on the thread that has been paused will continue immediately after the dispatch_semaphore_wait.
Now that we know the basics of using a semaphore, lets look at the sequence that we are going to pause/resume
To make things easy on us, we are going to use a real life example of a task that we can all relate to — unwrapping presents!
We will have a sequence of presents that get unwrapped in a depth-first search algorithmic way:
“One starts at the root (selecting some arbitrary node as the root in the case of a graph) and explores as far as possible along each branch before backtracking.” — Depth-first search Wiki
We are going to be the manager of unwrapping these presents, therefore we start by creating a singleton in Swift which will start, pause, resume and suspend the unwrapping presents process:
startUnwrapping() method implementation:
This protocol defines a blueprint for our presents. It requires an unwrap() method which will cause the present to start unwrapping itself and a completion handler which will inform the caller of the unwrap() method whether the present has been unwrapped successfully.
We mark the unwrap() method as mutating as we are going to change the otherwise immutable values of the struct to keep track of its unwrapping process.
We continue by calling the unwrappable parameter unwrap() method on a backround serial thread which gets executed asynchronously.
This is all done so that while we are unwrapping our presents on a serial background thread, we can pause it when we need to using a semaphore and we won’t end up blocking the main thread which updates the UI.
The structs that will conform to the Unwrappable protocol in our example will be of two kinds:
- Presents — group that holds presents and just keeps track of the present that gets currently unwrapped
- Present — struct that while it is being unwrapped we take time to check it and then move on to unwrapping the next present.
We are going to use the semaphore to pause the unwrapping process while a Present is being unwrapped, since Presents is just a concept that holds a group of presents.
To simulate the checking of a present we will create another serial background queue called the Check Serial Queue to which we are going to submit a block to be executed asynchronously. Inside the block we are going to cause the thread to sleep for a second and then check whether or not the Manager canUnwrap boolean is true. If it is — then we signal the semaphore to release the Unwrapping Serial Queue and continue with the next present.
When we pause the sequence from the Manager, we set the canUnwrap boolean to false and thus the semaphore remains negative and it will not resume the Unwrappable Serial Queue when the present Check Serial Queue finishes.
When we call resume, we set the canUnwrap to true and send a signal to the semaphore which then increments its count and the unwrapping process continues.
Where to go from here?
You can download the final project with all the code from this blog.