/ SWIFT

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: