Swift Atomic Properties with Property Wrappers
Atomicity is essential safety measure in concurrent environment, which ensures that your program runs predictably. Swift does not provide first-class language support for making properties atomic. In this article let’s fill in this gap by designing an atomic property wrapper.
Atomicity
The operation is atomic if it completes in a single step from the perspective of other threads. Without this safety measure you can never let different threads manipulate a shared variable at the same time. Otherwise you have a race condition, which results in an “undefined behavior”, e.g. random app crashes.
To enforce atomicity we can use locks, which are programming constructs that protect access to a given region of code at a time.
Swift offers different APIs for locking: NSLock
, os_unfair_lock
, pthread_rwlock_t
, DispatchSemaphore
, serial DispatchQueue
and OperationQueue
. To make a weighted choice, I have benchmarked and compared the aforementioned APIs and concluded that the serial dispatch queues are the best choice due to being fast and having high-level API.
Implementing Atomic Property Wrapper
Property wrapper is the Swift language construct that lets you define a custom implementation for a property and reuse it everywhere.
Let’s implement an atomic property wrapper based on serial DispatchQueue
:
@propertyWrapper
struct Atomic<Value> {
private let queue = DispatchQueue(label: "com.vadimbulavin.atomic")
private var value: Value
init(wrappedValue: Value) {
self.value = wrappedValue
}
var wrappedValue: Value {
get {
return queue.sync { value }
}
set {
queue.sync { value = newValue }
}
}
}
Now we can reuse the wrapper by applying it to any property:
struct MyStruct {
@Atomic var x = 0
}
var value = MyStruct()
value.x = 1
print(value.x) // 1
Although this covers single set
or get
operation, there is more involved to support both simultaneously.
Implementing Atomic Read-Write
Although get
and set
are individually atomic, the combination of both is not. Incrementing x
is not atomic, since it first calls get
and then set
:
var value = MyStruct()
value.x += 1 // ❌ Not atomic
The solution is to add the new method to our Atomic<Value>
property wrapper, which both reads and writes a value in a single step:
func mutate(_ mutation: (inout Value) -> Void) {
return queue.sync {
mutation(&value)
}
}
Since mutate()
cannot be accessed from the outside of MyStruct
, let’s declare a new increment()
method:
struct MyStruct {
@Atomic var x = 0
mutating func increment() {
_x.mutate { $0 += 1 }
}
}
var value = MyStruct()
value.increment() // `x` equals to 1
However, this approach does not scale well if we are to add new operations. Instead, we can expose the mutate()
method by utilizing projected properties of Swift property wrappers.
A property wrapper may provide a projection to expose more API by defining a
projectedValue
property [1].
To do so we need to make a couple of changes to Atomic<Value>
:
@propertyWrapper
class Atomic<Value> { // Changing `struct` into a `class`
var projectedValue: Atomic<Value> {
return self
}
// The `mutating` modifier is removed
func mutate(_ mutation: (inout Value) -> Void)
// The rest of the code is unchanged
}
Now we can access the mutate()
method:
struct MyStruct {
@Atomic var x = 0
}
var value = MyStruct()
value.$x.mutate { $0 += 1 } // `x` equals to 1
Dollar sign is the syntactic sugar to access the wrapper’s projected value.
When we update a value in a collection (Array
, Set
and Dictionary
), both get
and set
are called. Make sure to use the mutate()
method in such cases:
struct AnotherStruct {
@Atomic var x: [Int] = [1, 2, 3]
}
var value = AnotherStruct()
value.x[1] = 123 // ❌ This is not atomic
value.$x.mutate { $0[1] = 123 } // ✅ Atomic operation
The usage of Atomic<Value>
is not limited to properties. It can be used with any variable:
let one = Atomic(wrappedValue: 1)
one.mutate { $0 += 1 }
Further Reading
If you want to learn more about Swift locking APIs, I have some articles to suggest:
- Atomic Properties in Swift describes concurrency, multitasking and locks in more detail. It also provides the insight on how to use different locking APIs to enforce atomicity.
- Benchmarking Swift Locking APIs demonstrates how Swift locking APIs performs.
Thanks for reading!
If you enjoyed this post, be sure to follow me on Twitter to keep up with the new content. There I write daily on iOS development, programming, and Swift.