
axiom-synchronization
PopularUse when needing thread-safe primitives for performance-critical code. Covers Mutex (iOS 18+), OSAllocatedUnfairLock (iOS 16+), Atomic types, when to use locks vs actors, deadlock prevention with Swift Concurrency.
Use when needing thread-safe primitives for performance-critical code. Covers Mutex (iOS 18+), OSAllocatedUnfairLock (iOS 16+), Atomic types, when to use locks vs actors, deadlock prevention with Swift Concurrency.
Mutex & Synchronization — Thread-Safe Primitives
Low-level synchronization primitives for when actors are too slow or heavyweight.
When to Use Mutex vs Actor
| Need | Use | Reason |
|---|---|---|
| Microsecond operations | Mutex | No async hop overhead |
| Protect single property | Mutex | Simpler, faster |
| Complex async workflows | Actor | Proper suspension handling |
| Suspension points needed | Actor | Mutex can't suspend |
| Shared across modules | Mutex | Sendable, no await needed |
| High-frequency counters | Atomic | Lock-free performance |
API Reference
Mutex (iOS 18+ / Swift 6)
import Synchronization
let mutex = Mutex<Int>(0)
// Read
let value = mutex.withLock { $0 }
// Write
mutex.withLock { $0 += 1 }
// Non-blocking attempt
if let value = mutex.withLockIfAvailable({ $0 }) {
// Got the lock
}
Properties:
- Generic over protected value
Sendable— safe to share across concurrency boundaries- Closure-based access only (no lock/unlock methods)
OSAllocatedUnfairLock (iOS 16+)
import os
let lock = OSAllocatedUnfairLock(initialState: 0)
// Closure-based (recommended)
lock.withLock { state in
state += 1
}
// Traditional (same-thread only)
lock.lock()
defer { lock.unlock() }
// access protected state
Properties:
- Heap-allocated, stable memory address
- Non-recursive (can't re-lock from same thread)
Sendable
Atomic Types (iOS 18+)
import Synchronization
let counter = Atomic<Int>(0)
// Atomic increment
counter.wrappingAdd(1, ordering: .relaxed)
// Compare-and-swap
let (exchanged, original) = counter.compareExchange(
expected: 0,
desired: 42,
ordering: .acquiringAndReleasing
)
Patterns
Pattern 1: Thread-Safe Counter
final class Counter: Sendable {
private let mutex = Mutex<Int>(0)
var value: Int { mutex.withLock { $0 } }
func increment() { mutex.withLock { $0 += 1 } }
}
Pattern 2: Sendable Wrapper
final class ThreadSafeValue<T: Sendable>: @unchecked Sendable {
private let mutex: Mutex<T>
init(_ value: T) { mutex = Mutex(value) }
var value: T {
get { mutex.withLock { $0 } }
set { mutex.withLock { $0 = newValue } }
}
}
Pattern 3: Fast Sync Access in Actor
actor ImageCache {
// Mutex for fast sync reads without actor hop
private let mutex = Mutex<[URL: Data]>([:])
nonisolated func cachedSync(_ url: URL) -> Data? {
mutex.withLock { $0[url] }
}
func cacheAsync(_ url: URL, data: Data) {
mutex.withLock { $0[url] = data }
}
}
Pattern 4: Lock-Free Counter with Atomic
final class FastCounter: Sendable {
private let _value = Atomic<Int>(0)
var value: Int { _value.load(ordering: .relaxed) }
func increment() {
_value.wrappingAdd(1, ordering: .relaxed)
}
}
Pattern 5: iOS 16 Fallback
#if compiler(>=6.0)
import Synchronization
typealias Lock<T> = Mutex<T>
#else
import os
// Use OSAllocatedUnfairLock for iOS 16-17
#endif
Danger: Mixing with Swift Concurrency
Never Hold Locks Across Await
// ❌ DEADLOCK RISK
mutex.withLock {
await someAsyncWork() // Task suspends while holding lock!
}
// ✅ SAFE: Release before await
let value = mutex.withLock { $0 }
let result = await process(value)
mutex.withLock { $0 = result }
Why Semaphores/RWLocks Are Unsafe
Swift's cooperative thread pool has limited threads. Blocking primitives exhaust the pool:
// ❌ DANGEROUS: Blocks cooperative thread
let semaphore = DispatchSemaphore(value: 0)
Task {
semaphore.wait() // Thread blocked, can't run other tasks!
}
// ✅ Use async continuation instead
await withCheckedContinuation { continuation in
// Non-blocking callback
callback { continuation.resume() }
}
os_unfair_lock Danger
Never use os_unfair_lock directly in Swift — it can be moved in memory:
// ❌ UNDEFINED BEHAVIOR: Lock may move
var lock = os_unfair_lock()
os_unfair_lock_lock(&lock) // Address may be invalid
// ✅ Use OSAllocatedUnfairLock (heap-allocated, stable address)
let lock = OSAllocatedUnfairLock()
Decision Tree
Need synchronization?
├─ Lock-free operation needed?
│ └─ Simple counter/flag? → Atomic
│ └─ Complex state? → Mutex
├─ iOS 18+ available?
│ └─ Yes → Mutex
│ └─ No, iOS 16+? → OSAllocatedUnfairLock
├─ Need suspension points?
│ └─ Yes → Actor (not lock)
├─ Cross-await access?
│ └─ Yes → Actor (not lock)
└─ Performance-critical hot path?
└─ Yes → Mutex/Atomic (not actor)
Common Mistakes
Mistake 1: Using Lock for Async Coordination
// ❌ Locks don't work with async
let mutex = Mutex<Bool>(false)
Task {
await someWork()
mutex.withLock { $0 = true } // Race condition still possible
}
// ✅ Use actor or async state
actor AsyncState {
var isComplete = false
func complete() { isComplete = true }
}
Mistake 2: Recursive Locking Attempt
// ❌ Deadlock — OSAllocatedUnfairLock is non-recursive
lock.withLock {
doWork() // If doWork() also calls withLock → deadlock
}
// ✅ Refactor to avoid nested locking
let data = lock.withLock { $0.copy() }
doWork(with: data)
Mistake 3: Mixing Lock Styles
// ❌ Don't mix lock/unlock with withLock
lock.lock()
lock.withLock { /* ... */ } // Deadlock!
lock.unlock()
// ✅ Pick one style
lock.withLock { /* all work here */ }
Memory Ordering Quick Reference
| Ordering | Read | Write | Use Case |
|---|---|---|---|
.relaxed |
Yes | Yes | Counters, no dependencies |
.acquiring |
Yes | - | Load before dependent ops |
.releasing |
- | Yes | Store after dependent ops |
.acquiringAndReleasing |
Yes | Yes | Read-modify-write |
.sequentiallyConsistent |
Yes | Yes | Strongest guarantee |
Default choice: .relaxed for counters, .acquiringAndReleasing for read-modify-write.
Resources
Docs: /synchronization, /synchronization/mutex, /os/osallocatedunfairlock
Swift Evolution: SE-0433
Skills: axiom-swift-concurrency, axiom-swift-performance
You Might Also Like
Related Skills

verify
Use when you want to validate changes before committing, or when you need to check all React contribution requirements.
facebook
test
Use when you need to run tests for React core. Supports source, www, stable, and experimental channels.
facebook
feature-flags
Use when feature flag tests fail, flags need updating, understanding @gate pragmas, debugging channel-specific test failures, or adding new flags to React.
facebook
extract-errors
Use when adding new error messages to React, or seeing "unknown error code" warnings.
facebook