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 namedAtomicRepresentable
. - The
AtomicOptionalWrappable
protocol 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 namedAtomic
with no additional qualifiers, such asManagedAtomic
,UnsafeAtomic
,InlineAtomic
or anything like that. - The module in which
Atomic
and 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 moduleAtomics
would create such a collision with existingswift-atomics
code that is important to avoid. The nameSynchronization
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-importedSwift
module, such as raw locks, semaphores, condition variables, and the like. WordPair
belongs in theSynchronization
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 forDouble
, which was an argument for changing the name fromDoubleWord
as used by theswift-atomics
module. However, the Language Steering Group believes thatWordPair
is a better name thanDoubleWord
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!