The Swift Concurrency Pyramid
A hierarchical classification of thread-safety patterns
Concurrent programming remains one of the most error-prone aspects of software development, with data races and deadlocks representing fundamental challenges in multi-threaded systems. Swift's evolution from Grand Central Dispatch to structured concurrency with actors represents a paradigm shift toward compile-time verification of thread safety. This paper presents a hierarchical taxonomy of Swift concurrency mechanisms, organized as a six-tier pyramid that runs from compiler-verified isolation at the apex down to designs that push the correctness burden back onto the programmer or trade clarity for convenience at the base. I analyze each tier's safety guarantees, performance characteristics, and appropriate use cases, arguing that strict structured concurrency with actors provides the best balance between safety and usability for production systems. The classification reveals that recent language additions, particularly isolated parameters and the Approachable Concurrency feature, represent diverging philosophies in language design: prioritizing either performance optimization or beginner accessibility. This taxonomy gives practitioners a systematic framework for selecting appropriate concurrency mechanisms and guides language designers in evaluating trade-offs between compile-time safety, runtime performance, and developer ergonomics.
Introduction
Concurrent programming has been characterized as one of the hardest problems in computer science, not because the underlying concepts are inherently complex, but because the gap between correct and incorrect concurrent code is often invisible until runtime failures occur. Traditional threading models place the burden of correctness entirely on the programmer, requiring manual synchronization through locks, semaphores, or atomic operations. A single missed lock acquisition or incorrect ordering can introduce data races that manifest non-deterministically, making debugging extraordinarily difficult.
The fundamental challenge lies in the temporal dimension of concurrent execution. Unlike sequential programs where statements execute in a well-defined order, concurrent programs permit arbitrary interleaving of operations across threads. Without proper synchronization, two threads accessing shared mutable state create race conditions where the program’s behavior depends on unpredictable scheduling decisions. Modern optimizing compilers and hardware memory models can reorder operations in ways that further complicate reasoning about program correctness.
Swift’s approach to this problem represents a fundamental departure from traditional concurrency models. Rather than providing low-level primitives and trusting programmers to use them correctly, Swift leverages its strong type system to verify thread safety at compile time. The language’s evolution from Grand Central Dispatch in its early versions to structured concurrency with async/await, and finally to strict concurrency checking, demonstrates a clear trajectory toward compiler-enforced safety guarantees.
This paper lays out that trajectory as a taxonomy, a six-tier pyramid ordered by how much of the correctness burden the compiler carries, and then uses it to argue where Swift’s most recent concurrency features genuinely help and where they quietly hurt.
Background and Related Work
The Concurrency Challenge
Concurrent programming introduces fundamental challenges that sequential programming does not face. The primary issue is shared mutable state: when multiple threads can both read and modify the same memory location, the program’s behavior becomes dependent on the precise interleaving of operations. Consider two threads incrementing a shared counter. What appears to be a simple operation (read the current value, add one, write the new value) actually consists of multiple machine instructions that can be interleaved arbitrarily.
Data races occur when two threads access the same memory location concurrently, at least one access is a write, and the accesses are not properly synchronized. The consequences range from silent data corruption to crashes, and the non-deterministic nature of race conditions makes them notoriously difficult to detect through testing. Traditional approaches to preventing races rely on mutual exclusion through locks, but this introduces new problems: deadlocks when lock acquisition ordering is inconsistent, priority inversion when high-priority threads wait for locks held by low-priority threads, and performance degradation from lock contention.
Beyond correctness issues, concurrent programming faces usability challenges. Programmers must reason about all possible interleavings of operations, track which locks protect which data, and ensure proper lock acquisition and release. The cognitive burden is substantial, and even experienced developers regularly introduce concurrency bugs.
Evolution of Concurrency Models
Early concurrent programming relied on low-level primitives like semaphores and mutexes, essentially exposing hardware synchronization mechanisms directly to programmers. This approach offered maximum flexibility but minimal safety guarantees. Grand Central Dispatch, introduced by Apple, improved the situation by abstracting thread management into dispatch queues, but still required manual reasoning about data access patterns.
The actor model, introduced by Hewitt (1973) and later popularized by Erlang (Armstrong, 2007), represents a different approach. Rather than sharing memory between threads, actors communicate through message passing. Each actor maintains its own private state and processes messages sequentially, eliminating shared mutable state entirely. Traditional actor systems like Erlang’s, however, lack compile-time verification that messages are handled correctly.
Recent languages have explored type-system-based approaches to concurrency safety, each with distinct trade-offs.
Rust employs a dual-trait system with Send and Sync (Rust ownership documentation). A type is Send if it can be transferred between threads (ownership can move), and Sync if it can be referenced from multiple threads simultaneously (shared immutable access is safe). Critically, Rust distinguishes between transferring ownership and sharing references, something Swift’s Sendable conflates. Rust’s approach provides maximum expressiveness but requires understanding the distinction between &T (shared reference), &mut T (exclusive mutable reference), and owned values. The borrow checker enforces that mutable references are exclusive at compile time, preventing data races through aliasing control rather than isolation boundaries.
Pony uses reference capabilities (Clebsch et al., 2015), a more fine-grained system than either Rust or Swift. Each reference has a capability that specifies both what the holder can do (read/write) and what guarantees exist about other references (isolated, immutable, shared). The six capabilities (iso, trn, ref, val, box, tag) form a lattice with subtyping relationships. This expressiveness comes at a cost: developers must understand capability recovery, viewpoint adaptation, and the interaction between capabilities and generics. Pony demonstrates that highly sophisticated type systems can provide strong guarantees, but the cognitive overhead has limited adoption.
Go takes the opposite approach: goroutines and channels provide structured concurrency primitives, but the language offers no compile-time race detection. The philosophy is “don’t communicate by sharing memory; share memory by communicating,” but this is convention rather than enforcement. Go’s race detector is a runtime tool that uses happens-before analysis (the Go memory model) to detect violations, providing safety through dynamic checking rather than static types, in the tradition of dynamic race detectors such as Helgrind (Nethercote & Seward, 2007). This makes Go more accessible but catches errors later in the development cycle.
Swift occupies a middle ground. Like Rust, it uses compile-time verification, but Swift’s approach is coarser-grained. Sendable indicates that a type can cross isolation boundaries safely, but does not distinguish between ownership transfer and shared access. Like Pony, Swift uses isolation (actors) to prevent concurrent access, but without the fine-grained capability system. Like Go, Swift provides structured concurrency with async/await (SE-0296), and unlike Go it enforces safety through types rather than conventions.
The key trade-off Swift makes is pragmatism over expressiveness. Rust’s Send/Sync split and Pony’s six capabilities provide more precision, enabling patterns Swift cannot express. Swift’s simpler model reduces cognitive overhead, making safe concurrency more accessible. The @unchecked Sendable escape hatch acknowledges that some safe patterns exceed the type system’s expressiveness, prioritizing adoption over theoretical completeness.
Type Systems and Concurrency
Swift’s approach turns on treating thread safety as a type-level property. Data that can be safely shared across threads has different characteristics than data that cannot. The Sendable protocol formalizes this distinction: types conforming to Sendable can be safely transferred across concurrency domains without synchronization.
For value types like structs, Sendable conformance is straightforward when all stored properties are themselves Sendable. The absence of identity means that copying a value creates genuinely independent instances with no shared state. For reference types like classes, the situation is more complex. A class can be Sendable only if it is immutable (all properties are constants) or if it implements its own internal synchronization.
The @unchecked Sendable marker, introduced in SE-0302, allows programmers to assert that a type is thread-safe even when the compiler cannot verify this automatically. This is necessary for interoperating with legacy code or implementing custom synchronization, but it places the burden of correctness back on the programmer. The tension between compile-time verification and practical necessity forms a central theme in this taxonomy.
Technical Foundations
Before presenting the pyramid taxonomy, we establish the technical foundations underlying Swift’s concurrency model. Understanding executor mechanics, memory ordering guarantees, and isolation semantics is essential for analyzing the safety properties of each tier.
The Executor Model
Swift’s concurrency runtime is built on the concept of executors, which abstract the scheduling of asynchronous work. Unlike traditional thread-based models where developers explicitly manage threads, Swift’s executor system decouples logical isolation from physical execution resources.
A serial executor processes its work items one at a time. This is the kind of executor an actor uses. (Not every executor is serial: the default global executor that drives ordinary async work runs jobs concurrently across a pool.) When an actor is created, it is associated with a serial executor. Crucially, this executor is usually not a dedicated thread or a private serial DispatchQueue. The runtime schedules the actor’s jobs onto the shared cooperative thread pool, and serialized access is guaranteed by the actor’s atomic state rather than by a lock or a queue of its own. All isolated methods on the actor are enqueued on its executor, guaranteeing serialized access to the actor’s state without explicit locking.
The critical operation in this model is the executor hop: when code crosses an isolation boundary, execution must switch from the current executor to the target’s executor. Consider an actor method call from non-isolated code:
Executor hop mechanism
actor Database {
private var cache: [String: Data] = [:]
func store(key: String, value: Data) {
// Runs on Database's executor
cache[key] = value
}
}
func clientCode() async {
let db = Database()
// Currently on global executor (or caller's executor)
await db.store(key: "foo", value: data)
// Executor hop occurred at 'await'
// Now back on original executor
} The await keyword marks a suspension point where a specific sequence unfolds: the current task suspends, capturing its continuation; the work is enqueued on the Database actor’s executor; when that executor processes the request, it executes store; on completion, the continuation is enqueued back on the original executor; and the task resumes there.
This mechanism has several implications. First, suspension points are explicit and visible through await, making it clear where state may change due to interleaving. Second, executors can be backed by different resources (threads, dispatch queues, custom schedulers), providing flexibility in execution strategy. Third, the cost of an executor hop is non-trivial: it includes enqueueing overhead, potential context switches, and cache effects from migrating between cores.
The MainActor is a special global actor backed by the main dispatch queue. Code isolated to MainActor executes on the main thread, making it safe to interact with UI frameworks that require main-thread execution. This is distinct from ordinary actors, which run on the cooperative thread pool rather than a dedicated queue of their own.
Memory Ordering and Sendable
Swift’s Sendable protocol is intimately connected to memory ordering guarantees. To understand why Sendable is necessary, we must consider how modern processors and compilers reorder operations.
On weakly-ordered architectures like ARM, reads and writes can be reordered unless explicit memory barriers are inserted. Without proper synchronization, one thread writing to memory may not be visible to another thread reading the same location in the order the source suggests. This is the hardware manifestation of data races. The reorderings a program must tolerate are codified in language-level memory models, the C11 and Java memory models being the canonical examples.
Swift’s approach is to enforce that data crossing executor boundaries is either immutable, internally synchronized, or isolated. Immutable value types whose properties are all Sendable can be copied safely, creating independent instances. Internally synchronized reference types, marked @unchecked Sendable, must implement their own memory barriers. And data that never crosses an isolation boundary in the first place need not be Sendable at all.
The executor hop provides an implicit memory barrier. When work is enqueued on an executor, a happens-before relationship (Lamport, 1978) is established: all writes before the enqueue are visible when the work executes. This is implemented through atomic operations in the executor’s job-scheduling machinery.
Sendable does more than prevent races. It establishes happens-before relationships. A type that is Sendable can be safely transferred between executors because it is either copied by value, creating independent state, or backed by internal synchronization that provides the necessary memory barriers.
Non-Sendable reference types cannot cross boundaries because the compiler cannot guarantee that the necessary memory barriers exist. Consider:
Memory ordering violation without Sendable
class Counter { // Not Sendable
var value: Int = 0
}
let counter = Counter()
Task { counter.value += 1 } // Write on Task's executor
Task { print(counter.value) } // Read on a different Task's executor
// Without synchronization, the read may see a stale value
// or even a partially-written value on some architectures If Swift allowed this code, there would be no happens-before relationship between the write and the read. The read might see the old value or the new value due to compiler or hardware reordering. Even on modern 64-bit platforms, which provide atomic loads and stores for an aligned Int, the absence of a happens-before edge is the real problem. The compiler and processor remain free to reorder the access or to elide it entirely, for example by hoisting the load out of a loop into a register, and under the language memory model a data race on a non-atomic is undefined behavior, strictly worse than reading a stale value. For larger types (structs with multiple fields), torn reads become a concrete risk, where part of the old value and part of the new value are observed at once.
By requiring Sendable, Swift ensures that either the type is copied (establishing independence) or the programmer has explicitly taken responsibility for synchronization through @unchecked Sendable.
Swift 6 relaxes the Sendable requirement in one important case, through region-based isolation (SE-0414) and sending parameters (SE-0430). If the compiler can prove that a non-Sendable value belongs to a disconnected region (no other reference to it survives), the value may be transferred across an isolation boundary exactly once, after which the source can no longer access it. This provides ownership transfer without shared access, partially narrowing the gap with the Rust Send/Sync distinction discussed above. We return to its consequences for Sendable’s design in Future Work.
Actor Reentrancy and Suspension
Actor reentrancy is one of the most subtle aspects of Swift’s concurrency model. Unlike traditional locks which block until released, actors suspend at await points, allowing other work on the same actor to execute.
Actor reentrancy scenario
actor BankAccount {
private var balance: Int = 100
func withdraw(amount: Int) async -> Bool {
guard balance >= amount else { return false }
// Suspension point, other actor methods can run here
await performExternalValidation(amount)
// Balance may have changed while suspended!
if balance >= amount {
balance -= amount
return true
}
return false
}
func emergencyWithdraw(amount: Int) {
balance -= amount // Synchronous - no suspension
}
} Between the initial balance check and the final withdrawal, emergencyWithdraw could execute, changing the balance. The actor’s executor still serializes access, so this is no data race. It is a logic error, and an easy one to miss if you do not anticipate the interleaving.
The design rationale for reentrancy is deadlock prevention. If actor methods blocked until completion, circular dependencies between actors would cause deadlocks:
Reentrancy prevents circular deadlocks
actor A {
func callB(b: B) async {
await b.callBack(a: self) // Would deadlock if blocking
}
func callback() async {
// Must be callable even if callB is active
}
} The trade-off is explicit: actors prevent data races but not state mutation during suspension. Developers must validate state after suspension points, a property the compiler cannot currently verify.
Some proposals suggest non-reentrant actors or reentrancy annotations, but these are not yet part of the language. The current model prioritizes deadlock freedom over preventing reentrancy issues, reflecting the judgment that deadlocks are harder to debug than reentrancy bugs.
Isolation Inheritance with #isolation
The #isolation mechanism, introduced in SE-0420 and building on the isolated parameters of SE-0313, allows functions to inherit the caller’s isolation context. This provides syntactic sugar for capturing the current isolation domain:
Isolation inheritance mechanism
// Desugared form
func process(data: Data, isolation: isolated (any Actor)?) async {
// Executes on the actor specified by 'isolation'
}
func process(
data: Data,
isolation: isolated (any Actor)? = #isolation // Sugared
) async {
// #isolation captures caller's isolation context
}
actor Worker {
func work() async {
// process inherits Worker's isolation
await process(data: someData) // No executor hop
}
} The isolation parameter functions as a compile-time proof that the function will execute on a specific actor. The compiler tracks isolation through the call graph, ensuring that non-Sendable types do not escape their isolation domain.
This mechanism enables zero-overhead state management. Rather than creating an actor for temporary state or using @unchecked Sendable with locks, state can remain in a simple class with isolated methods. The compiler verifies safety while eliminating executor hops.
The limitation is that the type cannot be stored in a place where its lifetime exceeds the current isolation. For example, isolated types cannot be stored in actor properties or captured by detached tasks. This restriction is how the compiler ensures safety without requiring Sendable.
The Concurrency Pyramid
We present a hierarchical classification of Swift concurrency mechanisms, organized as a pyramid with six tiers. The ordering ranks tiers by how much of the correctness burden the compiler carries: higher tiers place more of it on the compiler and less on the programmer. It ranks guarantees rather than prescribing that you always climb as high as possible. As the Practical Guidance below makes explicit, Tier 2 is the right default for most code, and Tier 1 is reserved for cases where its stronger guarantees or zero-overhead execution are specifically warranted. The ordering also bundles several axes that are formally orthogonal (see Orthogonal Dimensions): compile-time verification, structured versus unstructured execution, and runtime cost. It is therefore a practitioner’s heuristic rather than a total order along any single measurable property. Tier 6 is the clearest illustration. It provides the same data-race safety as Tier 2, as we argue in the Discussion, and it sits at the base because its inverted defaults carry performance and pedagogical hazards, though its guarantees match Tier 2’s.
█████ Tier 1 · non-Sendable + isolated params
█████████ Tier 2 · actors
█████████████ Tier 3 · @unchecked Sendable (async)
█████████████████ Tier 4 · locks / atomics
█████████████████████ Tier 5 · structured, non-strict
█████████████████████████ Tier 6 · MainActor-default (Approachable) The Swift Concurrency Pyramid. Upper tiers place more of the correctness burden on the compiler and less on the programmer. The ordering bundles several formally orthogonal axes and serves as a practitioner’s heuristic rather than a total order along any single property. Tier 6 is a special case: it provides the same data-race safety as Tier 2 and sits at the base for the performance and pedagogical hazards of its inverted defaults, though its guarantees match Tier 2’s.
The following table summarizes the tiers. “Race-safe” indicates whether the compiler guarantees data-race freedom (Compiler) or the programmer must (Programmer). “Structured” and “Strict” denote structured concurrency and strict Sendable checking.
| Tier | Mechanism | Race-safe | Structured | Strict | Typical use |
|---|---|---|---|---|---|
| 1 | Non-Sendable + isolated params | Compiler | Yes | Yes | Hot loops on one actor, zero-hop local state |
| 2 | Actors | Compiler | Yes | Yes | Default for shared mutable state |
| 3 | @unchecked Sendable (async) | Programmer | Yes | Yes | Custom synchronization, legacy interop |
| 4 | Locks / atomics (Mutex, Atomic) | Programmer | No | Yes | Low-level primitives, short critical sections |
| 5 | Structured, non-strict | Warns only | Yes | No | Migration only |
| 6 | MainActor-default (Approachable) | Compiler (≡ Tier 2) | Yes | Yes | UI-bound app code |
Tier 1: Nonisolated Structured Strict Concurrency
The apex of the pyramid represents the ideal: non-Sendable types with isolated parameters that inherit the caller’s isolation context (Massicotte, “Non-Sendable Types”). This mechanism, formalized through the isolation: isolated (any Actor)? = #isolation parameter pattern, provides compiler-enforced safety with zero runtime overhead.
Not all mutable state needs to be Sendable. State that remains within a single isolation domain throughout its lifetime can be safely mutated without synchronization. The isolated parameter mechanism allows methods on non-Sendable types to inherit the caller’s isolation context, ensuring that the method executes on the same actor (or lack thereof) as its caller.
Tier 1 earns the apex for more than performance. Because the compiler forbids the state from escaping its isolation domain, the set of code that can touch it is lexically bounded, a strictly smaller surface to reason about than an actor’s, which is reachable from any context holding a reference. And because the idiomatic Tier 1 method is synchronous, it contains no await and therefore no suspension window, which makes it immune to the reentrancy and interleaving errors that an actor (Tier 2) remains exposed to. It gives the same compiler-verified data-race freedom as Tier 2, with a smaller blast radius and no interleaving hazard. Tier 2 is the sensible default only because it buys a small step of convenience for a correspondingly small trade-off.
Tier 1: Non-Sendable class with isolated parameters
class OptimizationState { // Not Sendable
var energyHistory: [Double] = []
var gradientHistory: [[Double]] = []
var iterationCount: Int = 0
func recordIteration(
energy: Double,
gradient: [Double],
isolation: isolated (any Actor)? = #isolation
) {
// Runs on caller's isolation domain
energyHistory.append(energy)
gradientHistory.append(gradient)
iterationCount += 1
}
}
actor Optimizer {
func optimize() async {
let state = OptimizationState()
for iteration in 0..<10000 {
let energy = computeEnergy()
let gradient = computeGradient()
// No executor hop - runs directly on Optimizer's executor
state.recordIteration(energy: energy, gradient: gradient)
}
}
} This approach eliminates executor hops entirely. When an actor calls a method with an isolated parameter, the method runs directly on the actor’s executor without switching contexts. The compiler verifies that the type cannot escape the current isolation domain, preventing data races while maintaining optimal performance.
The pattern is particularly valuable for localized mutable state in computationally intensive operations. Rather than creating an actor to manage state that is only accessed from a single caller, the state can remain in a simple class with isolated parameter methods. This reduces unnecessary abstraction while maintaining safety guarantees.
Tier 2: Strict Structured Concurrency with Actors
Actors (SE-0306) represent Swift’s primary concurrency abstraction for shared mutable state. An actor is a reference type that serializes access to its state through an executor. All instance methods and properties are actor-isolated by default, meaning they can only be accessed asynchronously from outside the actor.
Tier 2: Actor with shared mutable state
public actor DataProcessor {
private var cache: [String: ProcessedData] = [:]
private var processingCount: Int = 0
public func process(id: String, input: Data) async -> ProcessedData {
// Automatically serialized - no manual synchronization needed
processingCount += 1
if let cached = cache[id] {
return cached
}
let result = await heavyComputation(input)
cache[id] = result
return result
}
public func getStatistics() -> (cached: Int, processed: Int) {
return (cache.count, processingCount)
}
}
public struct ProcessedData: Sendable {
let result: [Double]
let timestamp: Date
} The compiler enforces that data crossing actor boundaries must be Sendable. This prevents sharing of mutable state between actors, eliminating the primary source of data races. Within an actor, state can be freely mutated without synchronization because the executor guarantees serialized access.
Actor isolation introduces minimal runtime overhead. Each actor has an associated serial executor that schedules its work onto the cooperative thread pool. Crossing actor boundaries requires an executor hop: the calling task suspends, the work is enqueued on the target actor’s executor, and execution resumes when the actor processes the request.
Tier 2: Multiple actors coordinating safely
actor JobQueue {
private var pending: [Job] = []
func enqueue(_ job: Job) {
pending.append(job)
}
func dequeue() -> Job? {
return pending.isEmpty ? nil : pending.removeFirst()
}
}
actor Worker {
let queue: JobQueue
func processNextTask() async {
// Safe: Job is Sendable, crossing actor boundary is allowed
guard let job = await queue.dequeue() else { return }
await process(job)
}
} Actors excel at managing shared mutable state that needs to be accessed from multiple concurrent contexts. The isolation model provides clear boundaries between concurrent domains, making it easier to reason about program behavior.
Tier 3: Strict Structured Concurrency with Manual Synchronization
This tier encompasses async/await with strict Sendable checking but without actor isolation. The defining characteristic is that synchronization remains manual, but the code operates within structured concurrency’s task hierarchy. The most common pattern is classes marked @unchecked Sendable, where the programmer asserts thread safety but implements synchronization explicitly.
The critical distinction from Tier 2 is that the compiler cannot verify correctness of the synchronization strategy. The programmer must ensure thread safety through locks, atomics, or other mechanisms, marking the type @unchecked Sendable to satisfy the type checker. The @unchecked marker is an escape hatch: it tells the compiler “trust me, this is safe” without providing machine-checkable evidence.
This tier maintains structured concurrency’s benefits: async/await syntax, task hierarchies, cancellation propagation, and suspension points. Data race prevention, however, depends entirely on the programmer’s implementation rather than the compiler’s verification.
Tier 3: @unchecked Sendable with manual synchronization
final class Cache: @unchecked Sendable {
private let lock = NSLock()
private var storage: [String: Data] = [:]
// @unchecked asserts safety the compiler cannot verify;
// correctness rests on lock being used consistently.
func get(_ key: String) -> Data? {
lock.lock(); defer { lock.unlock() }
return storage[key]
}
func set(_ key: String, value: Data) {
lock.lock(); defer { lock.unlock() }
storage[key] = value
}
}
// Used from within structured concurrency
Task {
let cache = Cache()
cache.set("key", value: data) // synchronous; no await needed
} The @unchecked marker acknowledges a fundamental limitation of type systems: some patterns that are provably safe cannot be expressed in the type system’s vocabulary. Classes that implement their own internal synchronization through locks or atomic operations are genuinely thread-safe, but the synchronization mechanism is invisible to the type checker.
This tier also encompasses custom synchronization patterns built on top of Swift’s concurrency primitives. Custom property wrapper macros can implement copy-on-write semantics with internal locking, providing thread-safe access through compiler-generated _read and _modify accessors.
Tier 3: Custom synchronization pattern
// Custom @ThreadSafe macro pattern (not standard Swift)
class Counter {
@ThreadSafe var count: Int = 0
// Uses _read/_modify accessors with locks
func increment() async {
count += 1 // In-place mutation, thread-safe
}
}
// The macro generates synchronization code
// Programmer verifies correctness The critical distinction from Tier 2 is manual verification. The compiler enforces that types crossing concurrency boundaries are marked Sendable, but the Sendable conformance itself is unchecked. Correctness depends on the programmer’s implementation of thread safety rather than the compiler’s verification.
Tier 4: Unstructured Strict Concurrency with Manual Locks
This tier marks the transition from structured to unstructured concurrency. Code at this tier uses traditional blocking synchronization primitives (locks, semaphores, atomics) without async/await. Strict Sendable checking remains active, but the absence of structured concurrency eliminates task hierarchies, automatic cancellation, and suspension points.
The fundamental difference from Tier 3 is the abandonment of async/await. Tier 3 uses @unchecked Sendable within async contexts, maintaining structured task management. Tier 4 eschews async entirely, relying on synchronous blocking operations for synchronization. This eliminates executor hops, and it also eliminates cooperative scheduling.
Tier 4 often coexists with Tier 3: libraries may implement thread-safe types using locks (Tier 4 internally) while exposing async interfaces (Tier 3 boundary). The key distinction is whether the synchronization mechanism involves structured concurrency or traditional threading primitives.
Tier 4: Manual locks with strict checking
class ThreadSafeCounter: @unchecked Sendable {
private let lock = NSLock()
private var count: Int = 0
func increment() {
lock.lock()
defer { lock.unlock() }
count += 1
}
func getValue() -> Int {
lock.lock()
defer { lock.unlock() }
return count
}
}
// No async/await - synchronous blocking operations
let counter = ThreadSafeCounter()
counter.increment()
let value = counter.getValue() The absence of async/await means no structured task hierarchy, no automatic cancellation propagation, and no suspension points. Synchronization is explicit and immediate: when a lock is acquired, the calling thread blocks until the lock is available. This can be simpler to reason about for certain algorithms, but it loses the composability benefits of async/await.
Performance characteristics differ fundamentally from actors. Lock acquisition and release are synchronous operations with minimal overhead when uncontended, typically just a few atomic instructions. Contention can degrade performance significantly, however, through context switches and cache coherency traffic.
The critical trade-off is between blocking and suspending. Actors suspend at await, allowing the thread to do other work. Locks block, tying up the thread until released. For short critical sections, blocking is more efficient than suspension. For longer operations or high contention, actors’ cooperative scheduling provides better resource utilization.
Tier 4 vs Tier 3: Blocking vs Suspending
// Tier 4: Blocking with locks
class BlockingCache: @unchecked Sendable {
private let lock = NSLock()
private var storage: [String: Data] = [:]
func get(_ key: String) -> Data? {
lock.lock()
defer { lock.unlock() }
return storage[key] // Thread blocks if contended
}
}
// Tier 3: Suspending with async
class SuspendingCache: @unchecked Sendable {
private let lock = NSLock()
private var storage: [String: Data] = [:]
private let backing: BackingStore // async data source
func get(_ key: String) async -> Data? {
lock.lock()
if let hit = storage[key] { lock.unlock(); return hit }
lock.unlock()
// Genuine suspension: the thread is freed
let value = await backing.load(key)
lock.lock(); storage[key] = value; lock.unlock()
return value
}
}
// Tier 2: Actor - no explicit locks
actor ActorCache {
private var storage: [String: Data] = [:]
func get(_ key: String) -> Data? { storage[key] }
} The example illustrates the progression: Tier 2 (actors) provides compiler-verified safety with implicit serialization. Tier 3 maintains async/await structure but requires manual synchronization inside. Tier 4 eliminates async entirely, using pure blocking synchronization.
Tier 4: Modern synchronization via the Synchronization module
import Synchronization
// Mutex<T> (Swift 6, Synchronization module) supersedes NSLock and the
// deprecated OSAtomic family. It is unconditionally Sendable, so this class
// needs no @unchecked: the compiler verifies the boundary.
final class RequestCounter: Sendable {
private let count = Mutex<Int>(0)
func increment() {
count.withLock { $0 += 1 }
}
func value() -> Int {
count.withLock { $0 }
}
}
// For lock-free counters, Atomic<Int> (also in Synchronization)
// gives the same guarantee with explicit memory orderings.
// No async/await structure. This tier is appropriate for performance-critical sections requiring precise control over synchronization, particularly when interfacing with low-level code or implementing custom synchronization primitives. In modern Swift, the Synchronization module’s Mutex (SE-0433) and Atomic (SE-0410) types are the idiomatic building blocks here. Because both are unconditionally Sendable, they can be used without @unchecked, moving a class of previously manual-synchronization code back under compiler-checked boundaries.
Tier 5: Structured Concurrency
This tier represents Swift’s concurrency model before strict checking: async/await with structured tasks, but without compiler enforcement of Sendable requirements. Code at this tier uses modern concurrency syntax but lacks the safety guarantees of strict mode.
Tier 5: Structured concurrency without strict checking
// Non-strict mode (warnings only, not errors)
class Something { // Not Sendable, but allowed
var state: Int = 0
func mutate() { state += 1 }
}
Task {
let thing = Something()
await doWork(thing) // Passing non-Sendable type
// Compiles, but potential race condition
}
func doWork(_ obj: Something) async { obj.mutate() }
// Possible concurrent access The compiler may issue warnings about potential data races, but these are advisory rather than errors. Non-Sendable types can cross actor boundaries, and the compiler cannot prevent sharing of mutable state between concurrent contexts.
Tier 5: Migration scenario with warnings
// Legacy code being migrated
class LegacyCache {
var data: [String: Any] = [:] // Not Sendable
func store(_ key: String, _ value: Any) {
data[key] = value
}
}
// New async code calling legacy synchronous code
Task {
let cache = LegacyCache()
// Warning: LegacyCache is not Sendable
await processWithCache(cache)
} This tier exists primarily for migration purposes. The value lies in maintaining the structured benefits (task hierarchies, cancellation, suspension) while deferring the work of making all types Sendable-correct. It should be considered a transitional state rather than a sustainable approach for production code.
Tier 6: MainActor-Default (Approachable Concurrency)
The final tier changes defaults rather than weakening safety. The “Approachable Concurrency” umbrella in Swift 6.2 lets a module make MainActor isolation the default for unmarked code (SE-0466), and makes nonisolated async functions run on the caller’s executor rather than the global pool unless marked @concurrent (SE-0461). From a type-system standpoint its data-race guarantees are identical to Tier 2. We place it at the base of the pyramid because its inverted defaults trade a real class of performance and comprehension hazards for reduced annotation.
Tier 6: Approachable Concurrency with MainActor default
// @MainActor inferred everywhere
class ViewController {
func load() async { // @MainActor inferred
await fetchData() // Now runs on MainActor!
}
func fetchData() async { // @MainActor inferred
// Was background before, now main thread
}
@concurrent func background() async {
// Must use @concurrent to escape to background
}
} The motivation is practical. In iOS/macOS applications much code legitimately runs on the main thread, and under standard Swift 6 every such type and method must carry a @MainActor annotation. Making MainActor the default removes that annotation from the common case, and background work is instead marked explicitly. The mechanism reaches further than it first appears: even a nonisolated type’s async method inherits the caller’s isolation and needs @concurrent to reach a background executor.
Tier 6: nonisolated struct requiring @concurrent
// Even nonisolated structs need @concurrent for background execution
nonisolated struct PhotoProcessor {
@concurrent // Required to actually run on background!
func process(data: Data) async -> ProcessedPhoto? {
// Without @concurrent, inherits caller's MainActor
// Despite being in a nonisolated struct!
let sticker = extractSticker(from: data)
let colors = extractColors(from: data)
return ProcessedPhoto(sticker: sticker, colorScheme: colors)
}
}
// Standard Swift: process() runs on global executor
// Approachable: process() inherits caller's isolation,
// need @concurrent for background execution Context: the Swift 6 migration
Approachable Concurrency is best read against the migration that preceded it. From Swift 5.5 (2021) through 5.7, strict-concurrency checking was available behind an opt-in flag (SE-0337), but most codebases ignored the resulting warnings for the roughly three years before they became mandatory. When Swift 6 made strict checking the default in June 2024, that deferred work surfaced at once: projects faced large numbers of errors for non-Sendable types crossing isolation boundaries and shared mutable state without synchronization. The errors were genuine concurrency defects, but the one-shot cost created real adoption friction. MainActor-default responds to that pressure. Apple announced Swift 6.2 at WWDC in June 2025 and released it that September, enabling Approachable Concurrency by default in new application projects created with Xcode 26.
The cost of inverted defaults
The central hazard is that inverting the default can hide a correctness problem behind code that compiles:
Approachable Concurrency hides performance problems
// Standard Swift 6: This is an error - Data not Sendable
class DataProcessor {
var cache: [String: Data] = [:]
// Error: 'cache' is non-Sendable
func process() async { await heavyComputation() }
}
// Approachable Concurrency: No error, but wrong behavior
class DataProcessor {
var cache: [String: Data] = [:]
// @MainActor inferred
func process() async { await heavyComputation() }
} Under standard Swift 6 the first form is an error, and that error forces a decision: should DataProcessor be an actor, should cache be isolated, and where should process run? Under MainActor-default the second form compiles, @MainActor is inferred, and heavyComputation() runs on the main thread, a latent performance defect with no diagnostic. Two further costs follow. The model teaches an inverted intuition (that async functions run on the main thread unless told otherwise) when structured concurrency is designed to express the opposite. And the migration is asymmetric: moving from Swift 5 to strict Swift 6 is painful but yields correct code, whereas adopting MainActor-default is easy but defers the same reasoning to the point where the hidden main-thread work must eventually be found and fixed.
These costs concentrate outside UI-bound application code (servers, command-line tools, computational libraries, and apps with substantial background work), for which the inverted default is a poor fit. Because the feature is promoted as the default for new projects, developers in those domains may adopt it without weighing the trade-off. Comparable annotation reduction is achievable without inverting semantics: module-by-module strictness, tooling that bulk-applies @MainActor to UI code, fix-it diagnostics that suggest the right annotation, and isolation inference confined to a module’s internals all lower migration cost while leaving the language’s defaults intact. We take up the underlying design question, optimizing for one domain versus holding consistent semantics across all of them, in the Discussion.
Orthogonal Dimensions
The pyramid tiers exhibit variation along two orthogonal dimensions: structured versus unstructured concurrency, and strict versus non-strict type checking. Understanding these dimensions illuminates why certain combinations exist and others do not.
Structured Versus Unstructured Concurrency
Structured concurrency organizes asynchronous work into a hierarchy of tasks. Every task except the root has a parent task, and cancellation propagates from parents to children. Tasks must complete or be cancelled before their parent completes, preventing resource leaks from orphaned asynchronous operations.
This structure provides several benefits. Cancellation becomes automatic and guaranteed: when a parent task is cancelled, all child tasks are cancelled transitively. Resource cleanup follows lexical scoping rather than requiring manual tracking. Error propagation follows the task hierarchy, making it clear where errors are handled.
Structured concurrency task hierarchy
func processMultipleItems() async throws {
try await withThrowingTaskGroup(of: Result.self) { group in
for item in items {
group.addTask {
// Child task cancelled if parent is cancelled
return try await processItem(item)
}
}
for try await result in group { handle(result) }
// All child tasks complete before parent completes
}
} Unstructured concurrency, by contrast, permits arbitrary spawning of concurrent work with no guaranteed relationship between tasks. Detached tasks execute independently of their creation context. Callbacks can be invoked at arbitrary times with no structural relationship to the code that registered them.
The key technical difference lies in suspension. Structured concurrency’s async/await enables cooperative multitasking where execution can suspend at well-defined points, yielding control to other tasks. Unstructured concurrency typically relies on thread blocking or callback-based asynchrony, neither of which provides the composability of structured suspension.
Strict Versus Non-Strict Type Checking
Strict concurrency checking enforces at compile time that all data crossing concurrency boundaries is Sendable. The compiler analyzes the complete program to verify that shared mutable state cannot be accessed concurrently without synchronization. Violations are reported as errors, preventing compilation of potentially unsafe code.
Non-strict checking relaxes this requirement, treating Sendable violations as warnings rather than errors. This permits gradual migration of existing codebases to the new concurrency model without requiring simultaneous conversion of all types to Sendable.
The trade-off is between safety and migration feasibility. Strict checking provides strong guarantees but requires that the entire codebase, including dependencies, participates in the Sendable protocol system. Non-strict checking permits mixing safe and potentially unsafe code but offers no protection against data races.
Importantly, strictness is orthogonal to structure. Structured concurrency can exist without strict checking (Tier 5), while custom synchronization with locks can be made strict through proper Sendable annotations (Tier 4). The combination of structured and strict (Tiers 1–3) provides the strongest safety guarantees.
Discussion
Safety Guarantees and Limitations
Each tier provides different safety guarantees, and understanding these boundaries is crucial for practitioners. Tiers 1 and 2 provide the strongest guarantees: the compiler prevents data races through isolation and Sendable checking. They cannot prevent logic errors, deadlocks from circular actor dependencies, or performance issues from excessive serialization.
Tier 3’s @unchecked Sendable creates a verification gap. The compiler ensures that only Sendable types cross boundaries, but cannot verify that the Sendable conformance is correct. A class marked @unchecked Sendable without proper internal synchronization will compile successfully but exhibit race conditions at runtime.
Unsafe @unchecked Sendable - compiles but has races
class UnsafeCache: @unchecked Sendable {
private var storage: [String: Data] = [:]
// No synchronization - UNSAFE!
func get(_ key: String) -> Data? { return storage[key] }
// Race: concurrent reads during write
func set(_ key: String, _ value: Data) { storage[key] = value }
// Race: dictionary internals not thread-safe
}
// No compiler errors, but unsafe when used concurrently This code demonstrates the danger of @unchecked Sendable. Swift’s Dictionary is not thread-safe. Its internal structure (typically a hash table with buckets) can be corrupted by concurrent modifications. Consider the following interleaving:
Concrete race scenario in UnsafeCache
let cache = UnsafeCache()
// Thread 1: Writing "key1"
Task {
cache.set("key1", data1)
// 1. Compute hash of "key1"
// 2. Find bucket index
// 3. Check if bucket exists
// 4. Allocate new bucket if needed
// 5. Insert key-value pair
// 6. Update count
}
// Thread 2: Writing "key2" (concurrent)
Task {
cache.set("key2", data2)
// Concurrent execution of steps 1-6
// If both threads allocate buckets simultaneously,
// one allocation may be lost (memory leak)
// If both update count, one increment may be lost
}
// Thread 3: Reading during writes
Task {
let value = cache.get("key1")
// Could return wrong value, nil, or crash
} The specific failure modes are varied. Lost updates occur when two threads insert into the same bucket simultaneously and one insertion overwrites the other’s metadata. Structural corruption arises during resizing, where exceeding capacity forces new storage to be allocated and entries rehashed; a read that lands mid-resize may touch deallocated memory. Count inconsistencies follow from the same lack of atomicity, as concurrent increments of the dictionary’s element count can lose writes. And torn reads are possible on architectures where reading a pointer-sized value is not atomic, so a read overlapping a write can observe half of the old value and half of the new.
These are not theoretical concerns. They manifest as crashes, data corruption, and non-deterministic behavior in production systems. The compiler offers no protection because @unchecked Sendable explicitly disables verification.
The lower tiers provide progressively weaker guarantees. Tier 4’s manual locks require correct acquisition ordering to prevent deadlocks and proper usage to prevent race conditions, with the compiler providing no assistance. Tier 5 offers only dynamic checking of certain violations, catching some errors at runtime but permitting others to manifest as data corruption.
Tier 6’s safety properties are identical to Tier 2 from a type-system perspective, but the semantic changes to isolation inheritance can introduce unexpected performance characteristics. Code that performs heavy computation in an async function may unknowingly block the main thread, degrading UI responsiveness despite being syntactically correct.
Performance Considerations
Performance varies significantly across tiers. Tier 1’s isolated parameters eliminate executor hops entirely, providing optimal performance for localized mutable state. Tier 2’s actors introduce small overhead from executor hops, but the cost is negligible given the simplicity and safety.
Tier 3’s manual synchronization can be faster than actors for uncontended access but degrades under contention. Lock acquisition has lower latency than an executor hop when the lock is available, but contention forces context switches that are more expensive than actor serialization.
The structured versus unstructured dimension affects performance beyond just the cost of synchronization. Structured concurrency’s cooperative scheduling allows better resource utilization than thread blocking. An actor processing a long-running operation can suspend periodically, allowing other actors on the same executor to make progress. Manual locks provide no such mechanism.
Tier 6’s performance characteristics are particularly concerning. Forcing background-appropriate work onto the main thread can introduce UI latency that was not present in standard Swift concurrency, and because the isolation context is determined by the caller, library code must anticipate every use case when deciding whether to mark a function @concurrent.
Design Philosophy and Trade-offs
The pyramid reveals a spectrum of confidence in compile-time verification. From Tier 1 to Tier 5, each step relaxes what the compiler guarantees in exchange for flexibility: Tier 1 demands the most of the type system (isolated-parameter tracking) and returns zero-overhead safety, while lower tiers shift responsibility to the programmer.
Tier 6 sits off that spectrum. It keeps Tier 2’s guarantees but inverts the default isolation, and so encodes a different decision: whether a language should optimize for the most common domain in its ecosystem or hold consistent semantics across all domains. For UI-bound iOS/macOS code, where a large majority of code runs on the main thread, MainActor-default removes real annotation noise. For server, tooling, and compute-heavy code it does the opposite, letting background-appropriate work compile onto the main thread. Apple’s choice to promote it as the default for new projects is thus partly a statement of identity: Swift optimizing as an Apple-platform language rather than a domain-neutral one. That is a defensible position. Our taxonomy makes a narrower claim: the choice lies on a different axis from Tiers 1–5 and should be judged as such, rather than as a further step down a safety gradient.
Practical Guidance
For practitioners, the pyramid provides decision guidance. New code should begin at Tier 2 (actors plus Sendable types) as the default choice. This tier provides strong safety guarantees with acceptable performance for the vast majority of use cases. The clear isolation boundaries make code easier to reason about and maintain.
Tier 1 optimization should be considered when profiling reveals executor hops as a performance bottleneck, or where its tighter confinement is worth the extra annotation. This typically occurs in tight loops with extensive iterations where even small overhead becomes significant. The additional complexity of isolated parameters is justified by measurable performance improvement.
Movement to Tier 3 is warranted primarily for interoperability with existing code or implementing custom concurrency primitives. The use of @unchecked Sendable should be accompanied by clear documentation of the synchronization strategy and why it cannot be expressed through standard mechanisms.
Tier 4 is appropriate for performance-critical sections requiring precise control over synchronization, particularly in low-level systems code or when implementing the concurrency primitives that higher tiers build upon. The absence of structured concurrency makes it unsuitable for most application-level code.
Tier 5 should be viewed as purely transitional. Code remaining at this tier indefinitely misses the safety benefits that motivated Swift’s concurrency evolution. Migration to strict checking should be prioritized as technical debt reduction.
Tier 6 should be adopted deliberately rather than by default. For UI-bound application code its reduced annotation is a genuine convenience. For reusable libraries and background-heavy code it is a hazard, because the isolation context becomes caller-dependent (code’s execution domain can no longer be reasoned about without knowing every call site) and background work can silently run on the main thread. Teams facing a large Swift 6 migration are usually better served by the semantics-preserving strategies noted above (incremental per-module strictness, bulk @MainActor tooling, and prioritizing genuine race fixes over error-count reduction) than by inverting defaults, which defers the underlying reasoning rather than removing it.
Future Work
The Swift Concurrency Pyramid reveals several areas where future language development could strengthen safety guarantees or improve expressiveness. We identify a few promising directions for research and implementation.
Reentrancy Control
Actor reentrancy, while necessary for deadlock prevention, remains a source of logic errors that the compiler cannot detect, and several mechanisms could address it. Non-reentrant actors would introduce a syntactic marker (for example, @nonreentrant actor) that prevents concurrent execution of methods on the same instance, at the cost of potential deadlocks. Reentrancy annotations would attach method-level attributes specifying reentrancy requirements, letting the compiler verify that state is revalidated after suspension. Critical sections would mark regions that must execute without an intervening await, so that state observed before the region stays valid throughout. And state validation would have the compiler detect when mutable state is read before a suspension point and used after it, requiring explicit revalidation.
The challenge is providing safety without sacrificing composability. Overly restrictive models risk reintroducing the deadlock problems actors were designed to solve.
Refined Sendable Semantics
Swift’s Sendable conflates transferability (ownership can move between domains) and shareability (concurrent access is safe). Rust’s Send/Sync split shows the value of separating them. Swift 6 has already closed part of this gap: region-based isolation and sending parameters (see Technical Foundations) permit transferring a provably-unique non-Sendable value across a boundary without requiring Sendable, and conditional conformances already make generic types Sendable exactly when their parameters are. The open problems are narrower. An explicit shareable/transferable split would draw a first-class distinction in the spirit of Send/Sync, rather than leaning on a single Sendable axis supplemented by per-call-site region inference. Fuller move-only integration would let non-copyable (~Copyable) types interact with isolation so that unique-ownership transfer is expressible in the type itself rather than inferred at each boundary. And capability-based isolation, drawing on Pony’s reference capabilities, would express finer-grained sharing policies than a single Sendable bit.
The recurring risk is complexity. Swift’s current model succeeds partly through simplicity, and each refinement must earn its cognitive cost.
Compiler-Inferred Optimization
Tier 1’s isolated parameters require explicit annotation, but many usage patterns could be inferred. Isolation inference could detect when a non-Sendable type never escapes its isolation domain and apply isolated-parameter semantics on its own. Executor-hop elimination could identify sequential calls on the same actor and batch them, removing intermediate hops. Granular locking could, for classes with several independent properties, generate fine-grained locks rather than coarse actor isolation. And escape analysis could track object lifetimes to prove that references never outlive their isolation context, relaxing Sendable requirements where the proof holds.
These optimizations move complexity from the programmer to the compiler, aligning with Swift’s goal of safe-by-default concurrency without performance penalties.
Formalization and Verification
While Swift’s concurrency model has intuitive semantics, formal verification would strengthen confidence in it. An operational semantics would give a formal model of executor scheduling, task suspension, and isolation inheritance. A type-soundness proof would establish that well-typed programs cannot exhibit data races. A happens-before formalization would specify the memory-ordering guarantees across executor hops precisely. And model checking would provide tools for verifying actor systems against temporal properties such as deadlock freedom and liveness.
Formalization would clarify subtle interactions, such as reentrancy with detached tasks or the interaction between #isolation and global actors.
Empirical Evaluation
Future work should also include quantitative evaluation. Performance benchmarks would compare Tiers 1 through 4 systematically across workloads with varying contention, critical-section length, and concurrency levels. Adoption studies would analyze real-world Swift codebases to determine how the tiers are distributed and how projects migrate between them. Usability studies would measure, under controlled conditions, how well developers reason about concurrency at each tier. And bug analysis would track concurrency defects in Swift projects longitudinally, before and after strict-concurrency adoption.
Such studies would validate or refute the pyramid’s claims about safety, performance, and usability trade-offs.
Conclusion
The Swift Concurrency Pyramid provides a systematic framework for understanding the safety, performance, and complexity trade-offs inherent in concurrent programming. By organizing mechanisms into six tiers, from compiler-verified isolation to designs that relax those guarantees for flexibility or convenience, the taxonomy clarifies the costs and benefits of different approaches.
The pyramid reveals that modern Swift concurrency achieves its safety guarantees through two complementary mechanisms: structured task hierarchies that ensure resource cleanup and cancellation propagation, and strict type checking that prevents sharing of mutable state across concurrency boundaries. These mechanisms are orthogonal (code can have one without the other), but their combination provides the strongest guarantees.
The progression from Tier 1 through Tier 5 represents increasing programmer responsibility for ensuring correctness. Tier 1’s isolated parameters require the most from the compiler but provide zero-overhead safety with the tightest confinement. Tier 2’s actors balance safety with usability, making them the appropriate default for most code. Lower tiers trade compiler verification for implementation flexibility, useful in specific circumstances but inappropriate as default choices.
Tier 6 sits at the base for a different reason than the tiers above it. It is as safe as Tier 2 through the same mechanisms, but its inverted defaults trade annotation for main-thread execution that hides outside UI-bound code. It sharpens a tension the taxonomy makes precise, the tension between domain-specific defaults and domain-neutral semantics, and it does not mark a further loss of safety. For language designers, that is a more interesting lesson than any single tier: compile-time verification of concurrency correctness is achievable through careful type-system design, but the choice of defaults is where a language quietly declares which domain it is really for.
Swift’s concurrency model represents a significant advance in making concurrent programming safer and more accessible. By moving thread-safety verification from runtime to compile time, it eliminates a major source of non-deterministic bugs. The pyramid taxonomy presented here helps developers navigate the available mechanisms and make informed decisions about which tier best serves their requirements.
Acknowledgments
Thanks to the Swift community for its ongoing work on concurrency features and for the extensive documentation and analysis provided by community members. Matt Massicotte’s writing on non-Sendable types informed my understanding of the design space.
References
- C. Hewitt, P. Bishop, and R. Steiger. A Universal Modular ACTOR Formalism for Artificial Intelligence. Proc. 3rd International Joint Conference on Artificial Intelligence, pp. 235–245, 1973.
- J. Armstrong. Programming Erlang: Software for a Concurrent World. Pragmatic Bookshelf, 2007.
- S. Clebsch et al. Deny Capabilities for Safe, Fast Actors. Proc. 5th International Workshop on Programming Based on Actors, Agents, and Decentralized Control (AGERE!), pp. 1–12, 2015.
- L. Lamport. Time, Clocks, and the Ordering of Events in a Distributed System. Communications of the ACM, vol. 21, no. 7, pp. 558–565, 1978.
- J. Manson, W. Pugh, and S. V. Adve. The Java Memory Model. Proc. 32nd ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages (POPL), pp. 378–391, 2005.
- ISO/IEC 9899:2011. Information technology, Programming languages, C, §7.17 (Atomics), 2011.
- N. Nethercote and J. Seward. Valgrind: A Framework for Heavyweight Dynamic Binary Instrumentation. Proc. ACM SIGPLAN Conference on Programming Language Design and Implementation (PLDI), pp. 89–100, 2007.
- Apple Inc. Grand Central Dispatch (GCD) Reference.
- The Rust Programming Language. Understanding Ownership.
- The Go Programming Language. The Go Memory Model.
- M. Massicotte. Non-Sendable Types.
Swift Evolution proposals cited: SE-0296 (Async/await) · SE-0302 (Sendable and @Sendable closures) · SE-0306 (Actors) · SE-0313 (Improved control over actor isolation) · SE-0337 (Incremental migration to concurrency checking) · SE-0410 (Atomics) · SE-0414 (Region-based Isolation) · SE-0420 (Inheritance of actor isolation) · SE-0430 (sending parameter and result values) · SE-0433 (Synchronous Mutual Exclusion Lock) · SE-0461 (Run nonisolated async functions on the caller’s actor by default) · SE-0466 (Control default actor isolation inference)