Swift Atomics makes shared storage and memory ordering explicit
Swift Atomics supplies low-level operations through separate storage representations, with managed and manually managed wrappers for synchronization libraries.
Putting an integer behind a convenient property does not make concurrent increments safe. The implementation needs both an atomic operation and a storage model that permits concurrent access. Swift Atomics, announced on October 1, supplies those operations directly to Swift library authors. Its most useful design decision is keeping atomic storage distinct from ordinary values, so callers cannot accidentally treat the same location as a normal variable.
The package targets synchronization implementations and concurrent data structures. It is a low-level facility that can support more convenient abstractions. Choosing an atomic integer is therefore the beginning of a synchronization design. The program still needs a reason for its ordering choices and a contract for every other piece of state involved.
I like Swift's default emphasis on local mutation and explicit types. Atomics occupies a deliberately different part of the language's concurrency rules. Making that difference visible through dedicated wrappers is useful because shared mutation should require an identifiable operation at each access.
Atomic access has a defined exception to exclusivity
SE-0282 establishes interoperability with a C/C++-style concurrency memory model. Its amendment to exclusivity permits overlapping accesses when both are atomic, alongside the existing permission for overlapping reads. Ordinary concurrent read-write and write-write access remains invalid.
That distinction does not make an arbitrary variable safe merely because one operation uses an atomic instruction. The proposal leaves mixed atomic and nonatomic access to the same location undefined, apart from initialization and deinitialization. All accesses participating in the concurrent use must follow the atomic storage contract.
The proposal also separates the memory model from an API design. Its revised form does not introduce native synchronization types into the standard library. It explains how existing C operations and ordering constraints interact with Swift code. The new package provides a Swift-facing API on that basis, allowing library development without requiring the full API to enter the standard library first.
This distinction matters when choosing a dependency. Using Swift 5.3 supplies the language-model clarification associated with SE-0282. Importing the Atomics package supplies the wrappers and methods described here. The package can evolve on its own release schedule while remaining constrained by that memory model.
Managed storage gives a counter a stable home
The initial package README presents ManagedAtomic as a generic class. Its stored representation is separate from the corresponding ordinary Swift type. The integer inside a managed atomic counter is accessed through atomic methods, each requiring an explicit ordering argument.
A small counter example follows the launch API. The parallel operation finishes before the final load:
import Atomics
import Dispatch
let counter = ManagedAtomic<Int>(0)
DispatchQueue.concurrentPerform(iterations: 4) { _ in
for _ in 0..<100 {
counter.wrappingIncrement(ordering: .relaxed)
}
}
let total = counter.load(ordering: .relaxed)
The final value is four hundred. The example needs indivisible increments so that competing updates are not lost. It does not use the counter to publish another object or announce that unrelated memory is ready. That limited purpose is what makes a simple relaxed counter example instructive.
The wrapper also prevents an ordinary read of its hidden integer storage. A caller uses load to obtain a value. Separating the representation makes the access rule easier to enforce through the API and permits types whose atomic layout differs from their normal layout.
ManagedAtomic uses class allocation and reference counting for lifetime management. That is convenient when an atomic value can have its own object allocation. It also makes allocation overhead part of the choice, which matters inside data structures that may contain many atomic locations.
Unsafe storage exposes lifetime responsibility
UnsafeAtomic is a struct offering more control over where atomic storage lives. The announcement describes manually allocated memory and slices of managed buffer storage as possible locations. That flexibility places responsibility for storage validity on the implementation using it.
The distinction concerns lifetime as well as performance. An atomic operation cannot repair a pointer whose storage has already been released. A synchronization algorithm must keep the atomic location alive for every thread that can access it. The managed wrapper supplies a more convenient ownership arrangement, while the unsafe wrapper lets a specialized data structure integrate atomic storage into its own allocation strategy.
SE-0282 explains why passing &variable to a C atomic wrapper is not a general substitute. Swift's inout-to-pointer conversion is not C's address-of operator. It can use temporary storage, and the conversion itself counts as access to the original variable. Concurrent conversions can violate exclusivity before the atomic operation begins.
A stable storage location must consequently be established by the implementation. The proposal discusses manually allocated variables and managed-buffer APIs as ways to obtain suitable storage. This is an important reason to use the package's wrappers. They encode more than a spelling for a CPU instruction.
A lock inside a mutating method can be too late
The proposal gives another example of how Swift access semantics affect synchronization. A struct method declared mutating starts a write access to its value for the duration of the call. Acquiring a lock inside that method does not retroactively prevent overlapping accesses that began when several threads called it.
That example is useful because the implementation can look conventionally protected. The programmer sees a lock around the integer update, but the language-level access begins outside that protected region. The synchronization boundary must cover the access that the language actually defines.
Reference types can offer nonmutating methods that synchronize access to their internal state. Their implementation must still protect those stored properties correctly. The distinction explains why the package offers a managed class wrapper and a separately managed storage abstraction instead of presenting a normal mutable struct property as universally safe to share.
I find this an especially valuable part of the design documentation. It connects concurrency rules to ordinary method calls and parameter passing. Without that connection, a programmer can choose the right low-level operation and still construct an invalid surrounding access pattern.
Ordering is part of each operation
The API distinguishes load, store and update ordering types. A load accepts AtomicLoadOrdering, while a store accepts AtomicStoreOrdering. An exchange or compare-exchange uses update ordering, with an overload permitting separate success and failure orderings for comparison.
Those separate enums restrict choices to the orderings appropriate for each operation. The package follows a subset of the C/C++ orderings and does not expose consuming ordering. The initial README requires an explicit argument instead of assigning one universal default to every atomic operation.
That makes a review ask the relevant question at the call site. Does the operation only update a counter, or does it establish a relationship with accesses to other memory? The answer belongs to the algorithm. Giving every operation the same spelling without its ordering would conceal part of that algorithm's requirements.
Compare-exchange also returns both whether the exchange occurred and the original value observed. An algorithm can use the result to decide whether to retry. Combining a load and a later store in application code would not provide that same indivisible comparison-and-update operation.
Package versioning reflects the initial API
The launch README uses version 0.0.1 and explains that source stability is guaranteed only within minor versions before 1.0. It gives an upToNextMinor dependency requirement for callers wishing to avoid updates that may break source. Choosing a dependency range is therefore part of adopting this initial package, even when the underlying atomic operations have familiar semantics.
This matters particularly for a synchronization library wrapping the API. Its users may never import Atomics directly, but the wrapper still needs to build against the selected package version. A constrained range makes the expected API explicit while the package develops. The README also treats eventual standard-library adoption as future evolution work, without presenting that move as already decided or implemented.
References bring memory reclamation into scope
The package supports atomic strong references for classes opting in through AtomicReference. The implementation uses double-wide operations, and the README notes that these accesses are relatively expensive. Separate lazy-reference wrappers address the narrower case of a reference initialized lazily and then kept constant.
That specialization avoids treating every reference use as a general mutable atomic reference. A data structure's access pattern affects which wrapper is appropriate. Reference counting can help reclaim objects, but the atomic operation must still obtain and manage the reference consistently with concurrent replacement.
The package guarantees lock-free implementations without promising wait-free completion. Some operations can use compare-exchange loops, so one competing thread may need repeated attempts even while the system makes progress. That is a precise limit on the operation's progress guarantee. An application needing a bound for one particular caller cannot infer it from the absence of a lock.