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.