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: