Why your @Atomic property wrapper doesn’t work for collection types
Published on: April 20, 2020A while ago I implemented my first property wrapper in a code base I work on. I implemented an @Atomic
property wrapper to make access to certain properties thread-safe by synchronizing read and write access to these properties using a dispatch queue. There are a ton of examples on the web that explain these property wrappers, how they can be used and why it's awesome. To my surprise, I found out that most, if not all of these property wrappers don't actually work for types where it matters most; collection types.
Let's look at an example that I tweeted about earlier. Given this property wrapper:
@propertyWrapper
public struct Atomic<Value> {
private let queue = DispatchQueue(label: "com.donnywals.\(UUID().uuidString)")
private var value: Value
public init(wrappedValue: Value) {
self.value = wrappedValue
}
public var wrappedValue: Value {
get {
return queue.sync { value }
}
set {
queue.sync { value = newValue }
}
}
}
What should the output of the following code be?
class MyObject {
@Atomic var atomicDict = [String: Int]()
}
var object = MyObject()
let g = DispatchGroup()
for index in (0..<10) {
g.enter()
DispatchQueue.global().async {
object.atomicDict["item-\(index)"] = index
g.leave()
}
}
g.notify(queue: .main, execute: {
print(object.atomicDict)
})
The code loops over a range ten times and inserts a new key in my @Atomic
dictionary for every loop. The output I'm hoping for here is the following:
["item-0": 0, "item-1": 1, "item-2": 2 ... "item-7": 7, "item-8": 8, "item-9": 9]
Instead, here's the output of the code I showed you:
["item-3": 3]
Surely this can't be right I though when I first encountered this. So I ran the program again. Here's the output when you run the code again:
["item-6": 6]
Wait. What?
I know. It's weird. But it actually makes sense.
Because Dictionary
is a value type, every time we run object.atomicDict["item-\(index)"] = index
we're given a copy of the underlying dictionary because that's how the property wrapper's get
works, we modify this copy and then reassign this copy as the property wrapper's wrappedValue
. And because the loop runs ten times and then concurrently runs object.atomicDict["item-\(index)"] = index
we first get ten copies of the empty dictionary since that's its initial state. Each copy is then modified by adding index
to the dictionary for the "item-\(index)"
key which leaves us with ten dictionaries, each with a single item. Next, the property wrapper's set
is called for each of those ten copies. Whichever copy is scheduled to be assigned last will be the dictionaries final value.
Don't believe me? Let's modify the property wrapper a bit to help us see:
@propertyWrapper
public struct Atomic<Value> {
private let queue = DispatchQueue(label: "com.donnywals.\(UUID().uuidString)")
private var value: Value
public init(wrappedValue: Value) {
self.value = wrappedValue
}
public var wrappedValue: Value {
get {
return queue.sync {
print("executing get and returning \(value)")
return value
}
}
set {
queue.sync {
print("executing set and assigning \(newValue)")
value = newValue
}
}
}
}
I've added some print statements to help us see when each get
and set
closure is executed, and to see what we're returning and assigning.
Here's the output of the code I showed you at the beginning with the print statements in place:
executing get and returning [:]
executing get and returning [:]
executing get and returning [:]
executing get and returning [:]
executing get and returning [:]
executing get and returning [:]
executing get and returning [:]
executing get and returning [:]
executing get and returning [:]
executing get and returning [:]
executing set and assigning ["item-5": 5]
executing set and assigning ["item-7": 7]
executing set and assigning ["item-1": 1]
executing set and assigning ["item-0": 0]
executing set and assigning ["item-6": 6]
executing set and assigning ["item-3": 3]
executing set and assigning ["item-8": 8]
executing set and assigning ["item-4": 4]
executing set and assigning ["item-9": 9]
executing set and assigning ["item-2": 2]
executing get and returning ["item-2": 2]
["item-2": 2]
This output visualizes the exact process I just mentioned. Obviously, this is not what we wanted when we made the @Atomic
property wrapper and applied it to the dictionary. The entire purpose of doing this is to allow multi-threaded code to safely read and write from our dictionary. The problem I've shown here applies to all collection types in Swift that are passed by value.
So how can we fix the @Atomic
property wrapper? I don't know. I have tried several solutions but nothing really fits. The only solution I have seen that works is to add a special closure to your property wrapper like Vadim Bulavin shows in how post on @Atomic. While a closure like Vadim shows is effective, and makes the property wrapper play nicely with collection types it's not the kind of API I would like to have for my property wrapper. Ideally you'd be able to use the dictionary subscripts just like you normally would without thinking about it instead of using special syntax that you have to remember.
My current solution is to not use this property wrapper for collection types and instead us some kind of a wrapper type that is far more specific for your use case. Something like the following:
public class AtomicDict<Key: Hashable, Value>: CustomDebugStringConvertible {
private var dictStorage = [Key: Value]()
private let queue = DispatchQueue(label: "com.donnywals.\(UUID().uuidString)", qos: .utility, attributes: .concurrent,
autoreleaseFrequency: .inherit, target: .global())
public init() {}
public subscript(key: Key) -> Value? {
get { queue.sync { dictStorage[key] }}
set { queue.async(flags: .barrier) { [weak self] in self?.dictStorage[key] = newValue } }
}
public var debugDescription: String {
return dictStorage.debugDescription
}
}
If we update the code from the start of this post to use AtomicDict
it would look like this:
class MyObject {
var atomicDict = AtomicDict<String, Int>()
}
var object = MyObject()
let g = DispatchGroup()
for index in (0..<10) {
g.enter()
DispatchQueue.global().async {
object.atomicDict["item-\(index)"] = index
g.leave()
}
}
g.notify(queue: .main, execute: {
print(object.atomicDict)
})
This code produces the following output:
["item-2": 2, "item-7": 7, "item-4": 4, "item-0": 0, "item-6": 6, "item-9": 9, "item-8": 8, "item-5": 5, "item-3": 3, "item-1": 1]
The reason this AtomicDict
works is that we don't send copies of the dictionary to users of AtomicDict
like we did for the property wrapper. Instead, AtomicDict
is a class that users modify. The class uses a dictionary to get and set values, but this dictionary is owned and modified by one instance of AtomicDict
only. This eliminates the issue we had before since we're not passing empty copies of the initial dictionary around.
In Summary
This discovery and trying to figure out why the @Atomic
property wrapper doesn't work for collection types was a fun exercise in learning more about concurrency, value types and how they can produce weird but perfectly explainable results. I've not been successful in refactoring my own @Atomic
property wrapper to work with all types just yet but I hope that some day I will. If you have any ideas, please do let me know and run it through the relatively simple test I presented in this post.
If you have any feedback or questions about this post, don't hesitate to reach out to me on Twitter.