Atomic properties and Thread-safe data structure in Swift
A recurring challenge in programming is accessing a shared resource concurrently. How to make sure the code doesn’t behave differently when multiple thread or operations tries to access the same property. In short, how to protect from a race condition?
To build a Thread-safe data structure, we need to briefly walk through concurrency principles in Swift.
Concurrency describes the ability to execute a portion of code separately to the rest of a program. It can be to fetch data through the network or make a request in the database or anything else we can separately execute aside of the main thread (the user interface one). There are many ways to do this in Swift, although the logic is always the same.
The most common way in iOS to do is to use Grand Central Dispatch (GCD)
DispatchQueue.main.async {
print("Executed on main thread")
}
let networkQueue = DispatchQueue(label: "network", qos: .utility, attributes: .concurrent)
networkQueue.asyncAfter(deadline: .now() + 2.0, execute: {
print("Executed asynchronously on `network` thread after 2sec")
})
let backgroundQueue = DispatchQueue.global(qos: .background)
backgroundQueue.sync {
print("Executed synchronously in background")
}
This is great if we want to execute a block on its own or for simple changes between threads. However, if we wants to organize a bit more our tasks and the execution order, it can quickly create headache.
That’s where we could use a higher level of api, using Operation (previously NSOperation
) to create more complex group of execution, cancel, pause or resume part of a program until the completion of another. There are other ways to do concurrency in Swift, for instance using Thread (previously NSThread
) but you’ve got the idea.
For time being, Grand Central Dispatcher should be enough to realize what we want.
For this example, let’s use a Queue data structure.
struct Queue<T> {
private var elements: [T] = []
mutating func enqueue(_ value: T) {
elements.append(value)
}
mutating func dequeue() -> T? {
guard !elements.isEmpty else {
return nil
}
return elements.removeFirst()
}
}
We wants to create a program where the order of execution is important and shouldn’t be altered regardless of the thread enqueueing or dequeueing elements. How to make sure it’s still executed in the right order? In short, how to protect from a race condition?
We need to locks for this implementation.
Same as Concurrency, there are many ways to do it, like using NSLock
or pthread
but let’s see how to reuse GCD to lock the execution for both functions.
We could create a serial queue: a queue that executes one task at a time in the order they’ve been added. That fits perfectly our purpose. By default a DispatchQueue
is serial.
struct Queue<T> {
private let queue = DispatchQueue(label: "queue.operations")
private var elements: [T] = []
mutating func enqueue(_ value: T) {
queue.sync {
self.elements.append(value)
}
}
mutating func dequeue() -> T? {
return queue.sync {
guard !self.elements.isEmpty else {
return nil
}
return self.elements.removeFirst()
}
}
}
Why using
sync
instead ofasync
here?
Using sync
will make sure the calling thread will wait for the task to be finished where async
would move on to the next line execution. Also, Swift won’t allow it here since we use a Struct
, we can’t execute a mutation in an escaping block.
This works great limited for this implementation since we only use write access for both functions. However, if we extend to head
and tail
properties, we could read and write values from different thread and can create race condition and odd behaviors.
We can improve this using a concurrent queue with barrier to sync write access on that specific queue and turn those two properties atomic. Using concurrent queue will make sure it’s accessible concurrently through multiple threads.
As a reminder, atomic properties guarantee to be thread-safe for read and write and always returns a value where non-atomic ones improve performance but can’t guarantee a returned value.
struct Queue<T> {
private let queue = DispatchQueue(label: "queue.operations", attributes: .concurrent)
private var elements: [T] = []
mutating func enqueue(_ value: T) {
queue.sync(flags: .barrier) {
self.elements.append(value)
}
}
mutating func dequeue() -> T? {
return queue.sync(flags: .barrier) {
guard !self.elements.isEmpty else {
return nil
}
return self.elements.removeFirst()
}
}
var head: T? {
return queue.sync {
return elements.first
}
}
var tail: T? {
return queue.sync {
return elements.last
}
}
}
The given flag barrier
helps us to make the concurrent queue behaves as serial for a block execution. It’s executed with a delay to make sure all previous executions are completed.
Does it mean I should always create separate threads?
It could be tempted to create one foreach usage but too many separate queues or blocking can actually impact your app performance as a whole. For simpler usage, you can safely rely on a DispatchQueue.globale()
to execute background tasks.
In conclusion, we’ve seen how to improve a data structure with atomic properties and avoid race condition for a thread-safe usage and a more robust program.
That being said, and as always, there is no silver bullet, excessive usage of blocking thread could impact your app performance even if it’s for a safer code.
Documentation: