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
AtomicValueprotocol shall be namedAtomicRepresentable. - The
AtomicOptionalWrappableprotocol shall be namedAtomicOptionalRepresentable.
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 namedAtomicwith no additional qualifiers, such asManagedAtomic,UnsafeAtomic,InlineAtomicor anything like that. - The module in which
Atomicand its related API will be placed will be calledSynchronization. Module name collisions are something that Swift does not provide good tools for mitigating, and naming the standard moduleAtomicswould create such a collision with existingswift-atomicscode that is important to avoid. The nameSynchronizationalso 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-importedSwiftmodule, such as raw locks, semaphores, condition variables, and the like. WordPairbelongs in theSynchronizationmodule 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 forDouble, which was an argument for changing the name fromDoubleWordas used by theswift-atomicsmodule. However, the Language Steering Group believes thatWordPairis a better name thanDoubleWordon 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!