| name | axiom-synchronization |
| description | 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. |
| license | MIT |
| metadata | {"version":"1.0.0"} |
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)
let value = mutex.withLock { $0 }
mutex.withLock { $0 += 1 }
if let value = mutex.withLockIfAvailable({ $0 }) {
}
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)
lock.withLock { state in
state += 1
}
lock.lock()
defer { lock.unlock() }
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)
counter.wrappingAdd(1, ordering: .relaxed)
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 {
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
#endif
Danger: Mixing with Swift Concurrency
Never Hold Locks Across Await
mutex.withLock {
await someAsyncWork()
}
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:
let semaphore = DispatchSemaphore(value: 0)
Task {
semaphore.wait()
}
await withCheckedContinuation { continuation in
callback { continuation.resume() }
}
os_unfair_lock Danger
Never use os_unfair_lock directly in Swift — it can be moved in memory:
var lock = os_unfair_lock()
os_unfair_lock_lock(&lock)
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
let mutex = Mutex<Bool>(false)
Task {
await someWork()
mutex.withLock { $0 = true }
}
actor AsyncState {
var isComplete = false
func complete() { isComplete = true }
}
Mistake 2: Recursive Locking Attempt
lock.withLock {
doWork()
}
let data = lock.withLock { $0.copy() }
doWork(with: data)
Mistake 3: Mixing Lock Styles
lock.lock()
lock.withLock { }
lock.unlock()
lock.withLock { }
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