Race condition happens when 2 or more threads access the shared data at the same time and change its value at the same time resulting in unexpected state.
A simple example is the typical ATM withdraw problem.
ATM withdraw problem
Let’s say we have our own bank account system and we have ATMs depoloyed all over the town to make withdrawl easy.
Let’s write the implementation of the withdraw(::)
function
And let’s try to withdraw money now:
Since the user is withdrawing from atm1 first, then atm2, one after the other in sequence, the data is not being modified simultaneously by atm1 and atm2, so we have no problem. After withdrawing 600 first, the balance will be only 400. So it will not let the user withdraw 500. The output will look like this:
Request to withdraw 600 from account with balance 1000
balance 1000 enough ✅ to withdraw 600
withdrawn: 600, remaining balance: 400
Request to withdraw 500 from account with balance 400
balanceNotEnough
Concurrent withdrawl
Now, let’s imagine a hypothetical scenario where the user can withdraw money from two ATMs at the same time.
The output will look like this:
Request to withdraw 600 from account with balance 1000
Request to withdraw 500 from account with balance 1000
balance 1000 enough ✅ to withdraw 600
balance 1000 enough ✅ to withdraw 500
withdrawn: 500, remaining balance: 500
withdrawn: 600, remaining balance: -100
The user is able to withdraw 1100 from his account where initial balance was only 1000. Did we just create 100 out of the thin year? 🤔
In fact what happened is a bad design in the withdraw
function. We allow two threads to access the balance at the same time. When 500 is withdrawn first in the above output, the balance is 500. But the first thread to withdraw 600 has already checked the balance and seen it as 1000 which is enough to withdraw 600. So we ran into an undesired state, aka race condition.
Solution:
Solution 1: Double check
A quick solution is to check again after the long process, just before withdrawing the balance to see if the balance had changed.
Here we check again just before deducting the balance (aka giving money to the user) to check the balance. This would work in this context, but it’s not the optimal solution in all circumstances
Solution 2: Sequential Queue
By running the critical part of our code in sequential queue, i.e. the withdrawl can only be done one queue at a time, and in sequence, we can avoid the problem of resource sharing among threads.
Solution 3: Concurrent Queue with Dispatch Barrier flag
When dispatching a code block to a concurrent queue, you can assign a flag to it indicating that it is a barrier task, meaning that when it is time to execute this task, it should be the only executing item on the specified queue.
Solution 4: NSLock
It’s the traditional way of synchronising a part of code. A lock can be accessed by any thread, but once the lock is acquired by a thread, all other threads are stopped from entering that piece of program until the lock is released. Important thing with NSLock is to identify the critical part of the code and add lock before it, and release the lock once that part is done with.
When using locks, it’s important to be careful to avoid a deadlock situation if two threads both acquiring locks are waiting on each other.
Solution 5: Semaphore
What’s a semaphore?
Before answering that, let’s answer what’s lock, mutex and semaphore?
A lock is a way to allow only one thread to enter a piece of code. A lock is not shared with any other processes that the thread shares resources with.
A mutex is similar to lock, i.e. it is a lock to a shared resource, like database table, or criticial piece of code, but it can be shared across multiple processes.
A semaphore is similar to mutex, but it can allow x number of threads to simultaneously access the resource.
In Swift’s context, a semaphore is defined as DispatchSemaphore(value: Int)
, here the value
parameter determines how many threads can enter the resource at the same time. When the value is 1, it is essentially a mutex.
That doesn’t mean Mutex and Semaphore are same. Mutex means mutual exclusion. It’s intended to force mutual exclusiveness for the shared resource, where is Semaphore is more for signaling. More explanation can be found here
Back to our problem, if we want to solve the atm withdraw problem using Semaphore, we can only allow one thread to alter the account balance at one time.
Conclusion:
Regardless of the details of each of these solutions, the core idea remains the same, identify the critical section and make it accessible by only one thread at a time.
Some parts of this article were influenced by this post.