(Note: This post uses Swift 5.1.)
This weekend I was working on a framework for fetch operations, and was wanting an easy way to make access to a property atomic in order to prevent race conditions where multiple things try to access or change something at the same time, which could cause all sorts of issues.
After doing a bit of research, this ended up being a two-step process, but in the end it makes both creating and accessing this property easy and succinct.
Writing the wrapper
To start with, let’s write a struct that wraps any value type with a dispatch queue.
struct Atomic<Value> {
private lazy var queue: DispatchQueue = {
let appName = Bundle.main.object(forInfoDictionaryKey: kCFBundleNameKey as String) as! String
return DispatchQueue(label: "\(appName).AtomicQueue.\(Value.self)")
}()
private var _value: Value
var value: Value {
mutating get { queue.sync { _value } }
set { queue.sync { _value = newValue } }
}
init(_ value: Value) {
self._value = value
}
}
The DispatchQueue
essentially makes a “checkout line,” where anything that tries to either get
or set
the value first has to wait in line behind anything else that’s queued up to try to access it. I label it with the name of the app and its type for easier debugging purposes1.
The underlying _value
is only accessible within this struct, so there’s no danger of it being accessed by something outside without going through the queue (unless we were to re-write/add to it and mess up somewhere, but prefixing with the underscore makes that less likely).
It’s a little strange that we need to mark the getter of value
with mutating
; that’s because, as I understand it, adding something to the queue actually changes the queue, which changes the struct, and anything that does so must be marked mutating
so the compiler knows whether certain actions can be taken when using let
vs. var
instances of this struct.
Now if we want to make something atomic, all we need to do is:
var x: Atomic<Int> = Atomic(1)
// later...
x.value = 2
print(x.value)
// on another thread somewhere else at the same time...
x.value = 3
print(x.value)
As you might imagine, though, writing .value
every time we want to use it is probably going to get old really quickly.
This is where property wrappers save the day.
Making it a Property Wrapper
Essentially, a property wrapper is a new-ish Swift feature that allows us to build a wrapper object around another object and make access to the wrapped object quick & easy. This post explains things quite well.
Even using a small portion of their capabilities, this is a perfect use case for our Atomic
struct, and quite fortunately, it only requires a very minor rewrite of our previous code.
@propertyWrapper
struct Atomic<Value> {
private lazy var queue: DispatchQueue = {
let appName = Bundle.main.object(forInfoDictionaryKey: kCFBundleNameKey as String) as! String
return DispatchQueue(label: "\(appName).AtomicQueue.\(Value.self)")
}()
private var value: Value
var wrappedValue: Value {
mutating get { queue.sync { value } }
set { queue.sync { value = newValue } }
}
init(wrappedValue: Value) {
self.value = wrappedValue
}
}
The only things we’ve changed from before are:
- We’ve added the
@propertyWrapper
keyword immediately before the declaration of our type - We’ve renamed some variables (
_value
tovalue
,value
towrappedValue
)
That’s it! Now you might be thinking, “Okay June, but now I have to type even more characters every time I access it; .wrappedValue
has 8 more key-presses than .value
!2 Why would you do that to me?!”
Fear not, my dear, lazy reader. Firstly, @propertyWrapper
is sort of like a protocol that requires a non-private var .wrappedValue
, so we’re required to use that name exactly.
As for having to type more… well, check this out:
@Atomic var x: Int = 1
// later...
x = 2
print(x)
// on another thread somewhere else at the same time...
x = 3
print(x)
All we need to do now when we want to make something atomic is put the @Atomic
keyword before it, and then anything else that accesses it doesn’t even need to know that it’s a wrapped value, and you never need to access .wrappedValue
(in fact, I don’t think you can at least without a few more steps). I don’t know about you, but I found this to be awesome.
Now the only danger is avoiding the temptation to needlessly use it everywhere…
<UPDATE 2020-04-25>
Donny Wals made a great post about why this will not work with collections in Swift. Worth a reading and worth keeping in mind.