Race Condition
In a multithreaded environment, there are times when multiple threads access (data write action) the shared resources at the same time. This will lead to the data race and unexpected results.
Getting unexpected results due to multiple threads writing the shared resources at the same time is also called Race Condition.
Let us create a situation where multiple threads write to the shared resource at the same time.
Get set goo….
Let us create a sample iOS project. In the View Controller swift file let us have the mutable array variable named as unsafeArray
.
Let us also have a method named raceCondition
where the array is mutated by dispatching the array append task asynchronously to the global queue.
Call the raceCondition()
method in the viewDidLoad()
method as shown below:
In the above example, we could see that the method raceCondition()
is called in the viewDidLoad()
method.
Since the global queues are concurrent queues, multiple threads will perform the write operation on the unsafeArray
at the same time. This will lead to the data race. Sometimes this will also lead to an app crash.
Let us run the application and observe the crash as shown below:
In the above example, we could see that the application is crashed. Though error “EXC_BAD_ACCESS” would happen for many other reasons, race condition is also one of them that could cause this error.
We could also observe an unexpected output as shown below:
To detect the Race Condition, let us enable the “Thread Sanitizer” in the scheme as shown below:
Run the project again once the Thread Sanitizer
is enabled. Xcode will show the error in purple as shown below:
From the above output we could see that the xcode is indicating that the “Swift access race” which means an “unsynchronized access to the mutable resource by multiple threads“
More on this - https://developer.apple.com/documentation/xcode/swift-access-races
Now, click on the “Thread Sanitizer Warning” indicator present on the top in purple color just beside the normal warning indicator as shown below:
On clicking the purple thread sanitizer, we could see the different threads access at the left side of the xcode warning navigator as shown below:
In the above image, we could see that there are mutating access by closure 1
and closure 2
.
Mitigate Race Condition
One of the ways to mitigate the race condition is by using a Dispatch Barrier.
Dispatch Barrier can be used by creating a custom concurrent queue. Set the flags parameter of the queue to .barrier
in the write operations. This makes the queue to act like a serial queue during write operations. During read operation, it acts like a normal concurrent queue. This helps us to make the unsafe shared objects as safe.
In the above image we could see that the when the barrier tasks are running, the concurrent queue acts like a serial queue. Once the barrier task is completed, it again acts like a concurrent queue.
To make use of the Dispatch Barrier let us create some properties and methods like below:
1. Create a custom concurrent queue like below:
let barrierQueue = DispatchQueue.init(label: "com.myCustomConcurrentQueue", attributes: .concurrent)
2. Create a new variable safeArray
like below:
var safeArray: [Int] {
barrierQueue.sync {
unsafeArray
}
}
3. Create a method to update the unsafeArray
like below:
func updateArr(val: Int) {
barrierQueue.async(flags: .barrier) {
self.unsafeArray.append(val)
}
}
4. Update the previous raceCondition()
method like below:
func raceCondition() {
let queue = DispatchQueue.global()
let group = DispatchGroup()
group.enter()
queue.async {
self.updateArr(val: 10)
group.leave()
}
group.enter()
queue.async {
self.updateArr(val: 15)
group.leave()
}
group.enter()
queue.async {
self.updateArr(val: 20)
group.leave()
}
group.notify(queue: .main) {
print("The final safe array is - \(self.safeArray)")
}
}
Step1: In the step 1, we have created a custom concurrent queue.
Step2: We have created a new variable called safeArray.
This is a computed property where the contents of the unsafeArray
are returned synchronously by the custom queue.
Step3: We have the function updateArr
that accepts the variable as a parameter. The custom queue has the flags parameter set to .barrier.
The write operations to the unsafeArray
are dispatched asynchronously to the custom queue. Whenever the concurrent queue gets the barrier task, it acts like a serial queue. This will make sure that only one thread is accessing the unsafeArray
at any time.
Step4: In the raceCondition()
method, we have a global queue and dispatch group. The updateArr
method is dispatched asynchronously to the global queue. This will make sure the multiple threads will try to write to the unsafeArray
at the same time. Since the updateArr
method makes sure only one thread is updating the unsafeArray
, the race condition scenario is mitigated.
With all these changes, run the project and the output is as shown below:
Thus, the race condition is mitigated by using Dispatch Barrier.