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.
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:
AtomicValueprotocol shall be named
AtomicOptionalWrappableprotocol shall be named
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:
Atomic<T>type should be named
Atomicwith no additional qualifiers, such as
InlineAtomicor anything like that.
- The module in which
Atomicand 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
Atomicswould create such a collision with existing
swift-atomicscode that is important to avoid. The name
Synchronizationalso 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
Swiftmodule, such as raw locks, semaphores, condition variables, and the like.
WordPairbelongs in the
Synchronizationmodule 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
DoubleWordas used by the
swift-atomicsmodule. However, the Language Steering Group believes that
WordPairis a better name than
DoubleWordon 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.
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
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.
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
*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
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, 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
Sendable only when the contained type is.
Thanks to everybody for participating in both rounds of review!