[Accepted with modifications] SE-0410: Atomics

The second review of SE-0410: Atomics ended on January 2, 2024, and the Language Steering Group has decided to accept the proposal with modifications to the protocol names, but otherwise accepting as proposed.

Naming changes

The proposal reuses names from the swift-atomics package where possible to minimize migration cost for existing developers. The Language Steering Group does not foresee name changes to protocols, types, and methods as being an insurmountable issue for developers, so although the ease of that migration is a valid concern, we would like to prioritize establishing the best name possible for these new APIs. We accept the proposal with the following name changes:

  • The AtomicValue protocol shall be named AtomicRepresentable.
  • The AtomicOptionalWrappable protocol shall be named AtomicOptionalRepresentable.

Review discussion raised a number of alternatives, but the Language Steering Group noted that AtomicValue’s requirements all contain the string AtomicRepresentation in their names, and likewise AtomicOptionalWrappable’s requirement names all contain AtomicOptionalRepresentation, so adopting the -able variant of those suffixes keeps these protocol names related both to each other and to their requirement names.

Reviewers also explored the naming of the following APIs, which we accept as is from the proposal, as well as any other names not specifically mentioned:

  • The Atomic<T> type should be named Atomic with no additional qualifiers, such as ManagedAtomic, UnsafeAtomic, InlineAtomic or anything like that.
  • The module in which Atomic and its related API will be placed will be called Synchronization. Module name collisions are something that Swift does not provide good tools for mitigating, and naming the standard module Atomics would create such a collision with existing swift-atomics code that is important to avoid. The name Synchronization also allows the module to be a natural place to put other low-level synchronization primitives that we might not want to be generally available in the default-imported Swift module, such as raw locks, semaphores, condition variables, and the like.
  • WordPair belongs in the Synchronization module alongside the atomic APIs. The Steering Group believes its use should be tied to supporting double-wide atomics. Being in an explicitly-imported module somewhat mitigates concerns about code completion pollution when developers reach for Double, which was an argument for changing the name from DoubleWord as used by the swift-atomics module. However, the Language Steering Group believes that WordPair is a better name than DoubleWord on its own merits. WordPair emphasizes that the value is a pair of not-necessarily-related values, and the phrase "double word" has other common meanings, particularly its use on Windows to specifically mean a 32-bit value due to the platform's history.

Interactions with the ownership and exclusivity model

A lot of the discussion centered on the way atomics fit into the ownership model. Almost all of the Atomic type’s API is borrowing, because atomic operations are meant to be safe even when multiple operations happen simultaneously so exclusive access isn’t necessary. This has the surprising implication that an atomic’s contained value can be updated even when it appears in a let property or as part of an otherwise-immutable aggregate. For most types, the borrow model and the law of exclusivity govern whether a value is immutable or mutable, but atomics and other concurrency primitives reveal that the more fundamental property being governed is shared vs. exclusive access. Atomics don’t require exclusive access in order to be updated, and enforcing exclusivity by our usual non-thread-safe mechanism would be counterproductive and introduce unsafety, so in the vast majority of cases atomic properties ought to be declared as let properties.

Since there’ll be a natural inclination to think of atomics as being mutable and incorrectly declare them as var, the proposal includes a provision to raise a diagnostic preventing var declarations of Atomic type. There was vigorous debate over how much this expansion compromises the core promises of the language, how APIs can separate mutating and nonmutating access to their atomic state, and whether we should consider an alternative model. We could for instance say that var x: Atomic is treated as a shared-borrows-only declaration despite the var, and only allow atomic update operations on atomic fields visible as var. These alternative schemes break down in the face of abstraction and composition, though, since the special behavior of Atomic under ownership is still present when Atomic is used as a generic parameter or appears as a component of a noncopyable struct or enum. We would have to introduce new generic constraints and/or transitive type properties to mark these “atomically updatable” types separately in order to keep atomics and similar concurrent types fully separated from “normal” types. On the other hand, the “immutable vs. mutable really means shared vs. exclusive”model as proposed works within the framework we already have without surfacing a new end-user-level language concept. It also follows the footsteps of Rust, where atomics have behaved like this from before Rust 1.0, and Rust’s atomics don’t seem to have caused any catastrophic effects or lasting confusion in that community. The Language Steering Group believes that this is the right model for Swift as well, and accepts this aspect of the proposal as is.

Should Atomic never, sometimes, or always be Sendable?

The proposal states that Atomic conditionally conforms to Sendable when its element type does. Review discussion argued against this in both directions: some folks argued that atomics in any circumstance are a “you know what you’re doing” feature, and raw exposure to atomics would never meet the safety bar for fearless concurrency we’re trying to reach with Sendable checking and isolation control, therefore Atomic should never be Sendable and should always have to be marked, either inside of a containing type that declares itself as @unchecked Sendable or as a nonisolated(unsafe) field, an “I think I know what I’m doing, AUDITORS LOOK HERE” trail in the code. On the other hand, Atomic’s very purpose is to be shared across threads, and the programmer already has to take intentional steps importing the Synchronization module and writing Atomic into their code to use atomics. Having Atomic ever not be Sendable in that view arguably imposes additional busywork on nearly every use of the type for little practical safety gain. Rust in particular takes this permissive approach, where even though their unsafe pointer types *T and *mut T do not have the Send trait, their AtomicPtr type does unconditionally have the Send trait, based on the notion that you’re already in unsafe land and outside of the language’s protection.

Based on community feedback, the Language Steering Group has decided to accept the proposal as is, leaving the Atomic type as Sendable only when the contained type is. The authors of the proposal as well as existing users of the swift-atomics package had noted that their use of the package’s atomic types generally coincides with the need for @unchecked Sendable for reasons outside of the use of atomics, and that the sendability or lack thereof of Atomic itself has not been a major issue in their development. There was also strong concern that making Atomic always Sendable would make it too large of a hole in the concurrency model, allowing arbitrary values to be improperly shared across threads through an Atomic<UnsafePointer<T>> even when T itself isn’t Sendable. If Atomic is Sendable, then it’s also readily usable with Swift’s higher-level concurrency language features, such as being able to appear as a nonisolated let field of an actor. Having this “just work” for pointers or other non-sendable types without any warning is likely too big a hole. On the other hand, a field of Atomic<Int> or other sendable type could be useful as a way of storing counters or other atomic-update-sized values in an actor that can be accessed independently without acquiring the actor’s execution context. Although even these simple-seeming uses of atomics require manual care to get right, such as ensuring matching ordering between readers and writers, an ABA problem with a numeric value is not a direct memory safety error like it would be with a pointer (unless it is used with unsafe operations, such as converting an integer to and from a pointer), and as such isn’t “unsafe” as the standard library generally uses the term. As such, we believe the proposal as written strikes the correct balance, leaving Atomic as Sendable only when the contained type is.

Thanks to everybody for participating in both rounds of review!

42 Likes

It's good to see this conclude. As the length of the acceptance post suggests, this was a pretty tricky proposal; atomics are not a trivial addition to Swift and not nearly so simple as in lower-level languages like C. The conversation in the review thread was thought-provoking and (I think) very helpful. I appreciate the contributions of the reviewers, and of course the hard work of the proposal's authors. Thank you all!

13 Likes

not nearly so simple as in lower-level languages like C

Not nearly so simple in C either if we’re honest … :joy:

More seriously, yes, big thanks to @Alejandro and @Karoy_Lorentey for seeing this through.

8 Likes