[Second Review] SE-0410: Atomics

Huh. Wow!

I have a really difficult time understanding how the team arrived at this statement, as it goes directly against my lived experience using these constructs.

I did try to express this as clearly as I could, even within this review thread:

(Highlight added.)

To reiterate, I believe it is not at all practical to implement concurrent data structures by passing around naked borrows of Atomic instances, and Swift should not encourage such misuse.

Custom concurrent data structures must be built as named types that conform to @unchecked Sendable.

Having to spell out @unchecked Sendable for such types is in no way "additional busywork", "unnecessary roadblock", or "ceremony". To be frank, I find the repeated use of these dismissive qualifiers to be quite infuriating. In the LSG's studied opinion, what exactly is the purpose of @unchecked Sendable, then?

To me, the @unchecked attribute warns readers that the type implements manual synchronization, alerting them about the (immense) complexity of such code, and the highly elevated risk of bugs. Importantly, it does this without imposing any actual hurdles on the part of the author -- by simply adding @unchecked, the compiler gets out of the author's way.

To me, the only reason to even consider adding a Sendable conformance on struct Atomic is to allow the simplest possible use cases, where passing around naked atomics is in fact not entirely unreasonable -- with atomic integer counters being the flagship example.

The sendability of atomic pointers is utterly irrelevant to authors of concurrent data structures.

Artificially requiring Atomic<UnsafeMutablePointer<T>> to conform to Sendable would not just be pointless, but I strongly believe it would be actively harmful, by encouraging developers to treat these primitives as if they were actually safe to pass around like candy. These aren't candies -- they are poison!

Specifically, atomic pointers are subject to a litany of insanely difficult concurrency issues -- including the ABA problem, and the general issue of memory reclamation. In my opinion, waving these away as if they were only rarely relevant would be a regrettable mistake.

Note that an immediate (and quite urgent) followup to this proposal will be to ship atomic strong references, ported from the swift-atomics package, which provide a reliable solution to these issues.

Over-concentrating on atomic pointers would be a mistake. While it is crucial that we provide direct support for atomic pointers (as they are important primitives), in most cases they will emphatically not be the right building blocks for building concurrent data structures -- atomic strong references are a far more solid foundation to build on.

Atomic strong references are a generalization of AtomicLazyReference that allows arbitrary reassignments. Atomic strong references solve the ABA problem and they are able to safely release the instance they're holding after its last user goes away. They will provide a universal baseline solution to the problem memory reclamation, bringing Swift to (roughly) equal footing with garbage-collected languages. Swift's story on atomics is not really complete without them, because they are the constructs that put building concurrent data structures within reach to a sizable audience of Swift developers who don't plan on spending years to specialize on memory reclamation strategies.

Therefore, if we decide it's appropriate to do so, we can safely declare an atomic strong reference Sendable if its associated class type is Sendable -- in fact, the package has been doing exactly that. To be frank, applying the same conformance to raw atomic pointers seems like an absurd idea to me.

As I see it, the realistic choices are that we can either

  • simply not conform Atomic to Sendable at all (leaving the door open to adding it later), or
  • we can adopt the "usual" rules about container types, and have struct Atomic conform to Sendable when its value is Sendable. (After all, Atomic is semantically a container of a single item.)

The swift-atomics package has had two years of experience with the second choice. I don't remember receiving any feedback indicating that it was the wrong one. Strict concurrency checking has been a thing for a while now -- has anyone actually run into an issue with the Sendable conformances of swift-atomics?

Please let's not follow Rust in this regard.

FWIW, I'm happy with the names struct Atomic and WordPair.

On AtomicValue, I think it does makes sense to rename the protocol, as its requirements do not match the protocol that ships in the package. Although having to migrate code is never fun, I think some developers will want to maintain compatibility with both the old package and the new stdlib APIs, and it will be easier to conform to both protocols if they have distinct names. I think AtomicWrappable is a clumsy name, but it's workable. So is AtomicRepresentable. For fun, I'll throw Atomicable and Atomizable into the mix -- I don't think dictionary definitions make good API names, and frankly the prevalence of overlong compound names makes it difficult to read/understand Swift code sometimes.

On the module name: Synchronization made the most sense so far, as I expect there is a good chance we'll want to ship locks in the same module, too. (Like atomics, I don't think we should push locks into the default namespace of every Swift program -- although this is less about sheer complexity of these APIs, and more about the desire to discourage blocking APIs in general.)

Reusing the name Atomics from the package would be incredibly disruptive due to language-level design defects around module name conflicts. Whis is not necessarily a good argument (after all, it potentially applies to every new module that ever gets added to SDKs) but I think there is real difference between theoretical issues and a actual known problem that has no good workaround. So if we want to go this way, we'll need to tackle the resulting module name conflict somehow. (It's not just a technical issue either -- it'd be quite confusing that import Atomics would mean different things depending on context.)

6 Likes