Atomic Counters in Swift

Our primary mechanism for managing state in Swift is through property wrappers and locks. However, there are a few other options for managing state in concurrent environments. Here we’ll look at using the Atomic property wrapper for atomic counters accessed by multiple threads.

import Foundation

@propertyWrapper
struct Atomic<Value> {
    private var value: Value
    private let lock = NSLock()

    init(wrappedValue: Value) {
        self.value = wrappedValue
    }

    var wrappedValue: Value {
        get { lock.withLock { value } }
        set { lock.withLock { value = newValue } }
    }

    func mutate(_ mutation: (inout Value) -> Void) {
        lock.withLock { mutation(&value) }
    }
}

func main() {
    // We'll use an atomic integer type to represent our (always-positive) counter.
    @Atomic var ops: UInt64 = 0

    // A DispatchGroup will help us wait for all threads to finish their work.
    let group = DispatchGroup()

    // We'll start 50 threads that each increment the counter exactly 1000 times.
    for _ in 0..<50 {
        group.enter()
        DispatchQueue.global().async {
            for _ in 0..<1000 {
                // To atomically increment the counter we use mutate.
                _ops.mutate { $0 += 1 }
            }
            group.leave()
        }
    }

    // Wait until all the threads are done.
    group.wait()

    // Here no threads are writing to 'ops', but it's safe to read the value
    // even while other threads might be updating it.
    print("ops:", ops)
}

main()

We expect to get exactly 50,000 operations. Had we used a non-atomic integer and incremented it with ops += 1, we’d likely get a different number, changing between runs, because the threads would interfere with each other. Moreover, we’d get data race failures when running with Thread Sanitizer enabled.

$ swift run
ops: 50000

Next, we’ll look at locks, another tool for managing state in concurrent environments.