Concurrency & Thread Safety in Swift

Thoughts about thread safety, race conditions, dead locks in Swift

Sean Lin
Cubo AI
6 min readAug 4, 2019

--

Multi-Threading

As you might know, multi-threading is an execution model that multiple threads can be executed at the same time on a single CPU core. The operating system assigns small slices of computing time to each thread and switch between. If multiple CPU cores are available, then multiple threads can be executed truly in parallel. Due to the power of multithreading, the total time needed for multiple tasks can be significantly reduced.

In computer science, the concept of running several tasks at the same time is described as Concurrency. It is a crucial topic that you need to learn almost everywhere in modern software programming. And iOS framework provides a couple of APIs for concurrent programming, such as Grand Central Dispatch(GCD), NSOperationQueue and NSThread.

Common Challenges

While multithreading is powerful, it comes with great complexity and also introduces two common issues. One is race conditions and the other is deadlocks. Generally, these all are related to managing access to shared resources.

Race Conditions

A race condition is an undesirable situation that occurs when a system attempts to perform two or more operations at the same time. Critical race conditions often happen when the processes or threads depend on some shared state.

Example: Stock management

Imagine that we sell iPhones in two different App Stores, one is in US and the other is in Taiwan. And there are only 2000 iPhones in stock now. We have to confirm the stock balance before the phones are sold.

  1. If the balance is sufficient, start the transaction process. And It will take less than two seconds for our server to do some validations.
  2. After the validation process is completed, we will deduce the amount of iPhones in stock.
  3. If the balance is insufficient, the transaction is denied by our server.

After running the codes, you will find that things are not going as expected. Our customer was able to buy a number of iPhones more than the total balance in stock!!

And we were able to understand the scenario of how this happened through the log below.

Well, it occurs when two stores sell phones from the balance at the same exact time. The problem is that, when one store (thread A) checked the balance it was sufficient, but at the time of balance deduction, the balance was already changed by another store (thread B). As a result, it became insufficient.

That’s how a race condition works. 😉

Deadlocks

In concurrent computing, a deadlock is a state in which each member of a group is waiting for another member, including itself, to take action, such as sending a message or more commonly releasing a lock.

Example: Two locks wait for each other

Let me give you a quick example for deadlocks. In this case, we keep reading and writing two shared arrays on different dispatch queues at the same time.

After running the codes in Xcode playground, you will find that even though the loop runs 100 times, the tasks in the asynchronous block are executed less than 100 times. In addition, the whole process was just stopped and those remain tasks were not executed anymore!!

This problem is that, when by chance two threads execute the task respectively at the same time, thread 1 acquires a lock A, thread 2 acquires a lock B. Now lock A and lock B are both waiting for the other lock, but will never be able to acquire it. As you can see, line 12 was executed by 34 times, line 23 was executed by 18 times. And then line 13 and line 24 were not executed anymore. The process just got stuck. We end up in a deadlock. 😝

https://www.objc.io/issues/2-concurrency/concurrency-apis-and-pitfalls/#dead-locks

Currently, we are familiar with race conditions and deadlocks, but how do we avoid these problems?

Let’s move on to Thread Safety!

Thread Safety Strategy

Thread Safety

Thread safe is a concept in the context of multi-thread and it means any shared data is accessed by only one thread at any given time.

If you want to write/read access to a shared resource from different threads, you should take the thread safety into consideration. There are several methods to achieve thread safety in Swift. So let us fix the pervious race condition issue of stock control now.

  1. Dispatch Barriers

Dispatch barriers are a group of functions acting as a serial-style bottleneck when working with concurrent queues.

https://basememara.com/creating-thread-safe-arrays-in-swift/

The barrier flag ensures that the concurrent queue does not execute any other tasks while executing the barrier process. Once the barrier process finished, then the queue allows running other tasks simultaneously by default implementation. This means that all items submitted to the queue prior to the dispatch barrier must complete before the barrier task execute.

2. Dispatch Semaphore

A dispatch semaphore is an efficient implementation of a traditional counting semaphore. Here is the steps how semaphore works:

First, we create a semaphore and set its start value. This represents the number of things that can access the semaphore without needing the semaphore to be incremented.

Next, when we would like to access one shared resource, we send a request to its semaphore and wait on it. Once the semaphore notifies us, we can assume that the resource is ours and we can use it.

Last, if the resource is no longer necessary, we let the semaphore know by sending him a signal, allowing him to assign the resource to another thread.

3. NSLock

NSLock class provides a lock of the resource to synchronize the data. It means to lock it so that only one thread can access that part of the code at a time. That means if there are several threads that are waiting for the resource, only one thread can get access to this resource at a time. Others will get the access to the resource after unlocking.

On the other hand, it is very important that you should be careful of deadlocks when using NSLock. Calling the lock method twice on the same thread will lock up your thread permanently.

Output:

Using the same AppStore class code we used before, the output of above three ways will be :

Now the results are in line with our expectations !~🙌

Wrapping up

In this article, we quickly talk about some methods for achieving thread safety. It goes without saying that there is no single best way on this problem— all have their pros and cons — so decide what’s important to you.

To sum up, if you have a lot of different threads and tasks accessing a shared resource, you never want to modify that shared resource through all of threads. Because the result of that will be very unexpected and might eventually crash your application.

Thanks for reading! Happy coding ~ :D

reference:

--

--