What's the state of modify/yield?

To force atomic access to properties, I've been using the strategy described by objc.io in their blog post but I was expecting to replace the whole thing with a property wrapper once Swift 5.1 was released. The idea of an @Atomic wrapper is also cited as an example in the proposal, so I thought it could be implemented, but it seems that you can't actually do that because you cannot ensure that read-write-read processes actually happen atomically (the objc.io blog post explains this bit pretty clearly).

But the same blog post links to a talk by @Ben_Cohen about the modify accessor that could solve this problem. AFAICT the accessor is still private (referenced with _modify) and cannot be used because it's still not possible to yield anything. Are there updates on this? Is this supposed to ship when Swift gets concurrency primitives?

7 Likes

My understanding—and keep in mind that I'm not working on move-only stuff or coroutines—is that atomic anything will require compiler support when not explicitly working with pointers or global stored properties. Even with modify, Swift's formal model is still move-in/move-out with assumed/enforced exclusivity, not by-address. (modify gets it to move-in/move-out rather than copy-in/copy-out, but not all the way to by-address. That's still considered an optimization.)

@John_McCall would have the most definitive answer to this question.

7 Likes

Your answer is right.

4 Likes

The @Atomic wrapper makes an intriguing demonstration, but I don't think it would be appropriate to adopt it in the stdlib -- a pair of atomic getter/setters is almost never the right interface.

Consider this:

@Atomic var counter: Int = 0

counter += 1 // HAZARD -- this is not an atomic operation (not even with _modify)

The ideal interface for an atomic integer would not at all look like a regular Int; it would have a whole different set of atomic operations. These need to be extremely explicit in the source -- it is undesirable to hide a pair of atomic load/store operations behind property accessors like above.

var counter: AtomicInt = 0

let newValue = counter.incrementThenFetch(ordering: .relaxed) // OK -- this is guaranteed to be atomic

As of Swift 5.1, it isn't practical to implement such an atomic type yet. These aren't value types, and modeling them using classes would not be a satisfactory solution, either. (The allocation, indirection and reference counting overhead would likely overwhelm the actual atomic operations.)

However, like Jordan says, we can expose atomic operations on the pointees of unsafe pointer types, and we can also provide better ways to get direct pointers to individual stored properties of a reference type. (I already have an exploratory PR with some experiments in this area; expect a proper pitch in this forum soon!)

10 Likes

@John_McCall What about general purpose coroutines? Is the implementation for the _modify accessor sufficient for exposing those in the language?

2 Likes

I assume you're asking about generators. The underlying function-splitting implementation supports generators, but we'd need a SIL representation for them (which I've thought about) as well as some sort of source-language syntax (which I haven't thought about as much).

6 Likes

I too would like to know the state of modify/yield. We have a QueueConfined<T> type that protects access to a value using a dispatch queue. We were hoping to use property wrappers but need modify/yield for the mutation case. move-in/move-out is fine here because we'd have exclusive access to the value at the point of yielding it.

1 Like

Reading the above but not fully understanding how the distinction between move-in/move-out and by-address impacts this, I'm wondering under what circumstances using a lock in _modify would not be thread safe. What are the dangers in using an implementation like the one below (apart from the general question of whether an Atomic wrapper is the right solution in a particular case)?

@propertyWrapper
public class Atomic<Value> {
  private let lock = NSLock()
  private var value: Value
  
  public init(wrappedValue: Value) {
    self.value = wrappedValue
  }
  
  public var wrappedValue: Value {
    _read {
      lock.lock()
      defer { lock.unlock() }
      
      yield value
    }
    _modify {
      lock.lock()
      defer { lock.unlock() }
      
      yield &value
    }
    set {
      lock.lock()
      defer { lock.unlock() }
      
      value = newValue
    }
  }
  
  // Closure-based version of modify for when subscript access isn't appropriate.
  public func modify(_ updates: (inout Value) throws -> Void) rethrows {
    lock.lock()
    defer { lock.unlock() }
    
    try updates(&value)
  }
}
2 Likes

I wouldn't normally bump a topic like this, but I realize last week was an especially bad time to ask questions with so many people on a Thanksgiving break. So I hope people don't mind me trying to get this some additional attention.

1 Like

I don’t believe move-in/out versus by-address matters in this case. You’re maintaining a lock for the duration of the coroutine, so either approach should be safe. Move-in/out versus by-address should really only matter as an optimization for when the value is particularly large (move-in/out can already skip retains/releases so it’s really just a matter of copying the bytes around).