/ 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:


Thanks for reading!

If you enjoyed this post, be sure to follow me on Twitter to not miss any new content.

Vadim Bulavin

Creator of Yet Another Swift Blog. Senior iOS Engineer at Pluto TV. Coding for fun since 2008, for food since 2012.

Follow