2

I am implementing my own AtomicDictionary property wrapper as follows:

@propertyWrapper
public class AtomicDictionary<Key: Hashable, Value>: CustomDebugStringConvertible {
  public var wrappedValue = [Key: Value]()

  private let queue = DispatchQueue(label: "atomicDictionary.\(UUID().uuidString)",
                                    attributes: .concurrent)

  public init() {}

  public subscript(key: Key) -> Value? {
    get {
      queue.sync {
        wrappedValue[key]
      }
    }

    set {
      queue.async(flags: .barrier) { [weak self] in
        self?.wrappedValue[key] = newValue
      }
    }
  }

  public var debugDescription: String {
    return wrappedValue.debugDescription
  }
}

now, when I use it as follows:

class ViewController: UIViewController {
  @AtomicDictionary var a: [String: Int]

  override func viewDidLoad() {
    super.viewDidLoad()
    self.a["key"] = 5
  }
}

The subscript function of the AtomicDicationary is not called!!

Does anybody have any explanation as to why that is?

YanivH
  • 539
  • 4
  • 18
  • Consider to use an `actor`. And this queue cannot cause a retain cycle. – vadian Apr 17 '22 at 13:51
  • 3
    It seems you have misunderstood what a property wrapper is. The variable declaration is an AtomicDictionary property wrapper, so the variable's getter and setter pass thru it. But the wrapped value, ie the value of the variable, is a normal Dictionary. Perhaps what you wanted was an actual type, not a property wrapper at all. – matt Apr 17 '22 at 14:20
  • 1
    “I need to support things that are lower than iOS15, so I must use GCD.” … Technically, you _can_ use Swift concurrency with iOS 13 & 14, too. But that’s academic: It’s overkill to transition an app from GCD to Swift concurrency just for atomic behaviors… – Rob Apr 17 '22 at 16:44
  • @Rob What do you mean academic? If I will try to compile it, the compiler will throw an error. In addition, does libSwift on iOS13 and 14 support asyncio? – YanivH Apr 18 '22 at 11:03
  • 1
    I mean “academic” because despite Swift concurrency support in iOS 13 being introduced by Xcode 13.2, it doesn’t matter. Atomicity is a pretty weak rationale for transitioning to Swift concurrency, IMHO. And if you have other reasons for not transitioning to Swift concurrency quite yet, then that simplifies the decision process further. Obviously, if you are already using Swift concurrency, then definitely use actors for thread-safe data access. My point was merely that atomic behavior, alone, is not a great rationale for the transition to Swift concurrency. – Rob Apr 18 '22 at 15:28
  • 1
    By the way, while Xcode 13.2 introduced backward compatibility of Swift concurrency, probably needless to say, it does not provide backward support of the all the new `async` Foundation/UIKit API introduced in iOS 15. So you get actors, `async`-`await`, and `Task` for your own asynchronous code, but not all the related Foundation API (e.g., not the `URLSession` `async` methods). – Rob Apr 18 '22 at 16:00

1 Answers1

1

Property wrappers merely provide an interface for the basic accessor methods, but that’s it. It’s not going to intercept subscripts or other methods.

The original property wrapper proposal SE-0258 shows us what is going on behind the scenes. It contemplates a hypothetical property wrapper, Lazy, in which:

The property declaration

@Lazy var foo = 1738

translates to:

private var _foo: Lazy<Int> = Lazy<Int>(wrappedValue: 1738)
var foo: Int {
    get { return _foo.wrappedValue }
    set { _foo.wrappedValue = newValue }
}

Note that foo is just an Int computed property. The _foo is the Lazy<Int>.

So, in your a["key"] = 5 example, it will not use your property wrapper’s subscript operator. It will get the value associated with a, use the dictionary’s own subscript operator to update that value (not the property wrapper’s subscript operator), and then it will set the value associated with a.

That’s all the property wrapper is doing, providing the get and set accessors. E.g., the declaration:

@AtomicDictionary var a: [String: Int]

translates to:

private var _a: AtomicDictionary<String, Int> = AtomicDictionary<String, Int>(wrappedValue: [:])
var a: [String: Int] {
    get { return _a.wrappedValue }
    set { _a.wrappedValue = newValue }
}

Any other methods you define are only accessible through _a in this example, not a (which is just a computed property that gets and sets the wrappedValue of _a).


So, you’re better off just defining a proper type for your “atomic dictionary”:

public class AtomicDictionary<Key: Hashable, Value> {
    private var wrappedValue: [Key: Value]
    
    private let queue = DispatchQueue(label: "atomicDictionary.\(UUID().uuidString)", attributes: .concurrent)
    
    init(_ wrappedValue: [Key: Value] = [:]) {
        self.wrappedValue = wrappedValue
    }
    
    public subscript(key: Key) -> Value? {
        get {
            queue.sync {
                wrappedValue[key]
            }
        }
        
        set {
            queue.async(flags: .barrier) {
                self.wrappedValue[key] = newValue
            }
        }
    }
}

And

let a = AtomicDictionary<String, Int>()

That gives you the behavior you want.


And if you are going to supply CustomDebugStringConvertible conformance, make sure to use your synchronization mechanism there, too:

extension AtomicDictionary: CustomDebugStringConvertible {
    public var debugDescription: String {
        queue.sync { wrappedValue.debugDescription }
    }
}

All interaction with the wrapped value must be synchronized.


Obviously you can use this general pattern with whatever synchronization mechanism you want, e.g., the above reader-writer pattern, GCD serial queue, locks, actors, etc. (The reader-writer pattern has a natural appeal, but, in practice, there are generally better mechanisms.)


Needless to say, the above presumes that subscript-level atomicity is sufficient. One should always be wary about general purpose thread-safe collections as often the correctness of our code relies on a higher-level of synchronization.

Rob
  • 415,655
  • 72
  • 787
  • 1,044