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?
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.
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!)
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).
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.
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)
}
}
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.
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).