Multithreading in iOS – Part 6

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:

1. Race Condition Creation

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:

2. Race Condition crash

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:

3. Race Condition Output

To detect the Race Condition, let us enable the “Thread Sanitizer” in the scheme as shown below:

4. Thread Sanitizer

Run the project again once the Thread Sanitizer is enabled. Xcode will show the error in purple as shown below:

5. Thread Sanitizer Output

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:

6. Thread Sanitizer warning indicator

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:

Thread Sanitizer warning indicator
7. Warning indicator

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.  

8. Dispatch Barrier

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:

9. Safe Array Output

Thus, the race condition is mitigated by using Dispatch Barrier.

Leave a Comment

Your email address will not be published. Required fields are marked *