Why your @Atomic property wrapper doesn’t work for collection types

Published by donnywals on

A 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.


Practical Combine

Learn everything you need to know about Combine and how you can use it in your projects with my new book Practical Combine. You'll get thirteen chapters, a Playground and a handful of sample projects to help you get up and running with Combine as soon as possible.

The book is available as a digital download for just $24.99!

Get Practical Combine

Receive weekly updates about my posts

Categories: Swift