🔦

Swift 6 Thread Safety: Replace @unchecked Sendable with Mutex

TL;DR: In Swift 6, building thread-safe reference types no longer requires a compromise between actor (forced async usage) and NSLock + @unchecked Sendable (loss of compiler safety). The new Synchronization framework provides a perfect solution for synchronous thread safety.

Creating a thread-safe reference type (class) has long been a pain point for Swift developers in concurrent code. If you opt out of using an actor (often because you want to avoid turning every API into an async function), the common workaround has been manual locking combined with @unchecked Sendable to force the compiler to accept the code. However, this merely silences the compiler errors—if the lock implementation is flawed, data races can still occur at runtime.

Swift 6 introduces the Synchronization framework, featuring Mutex—an official synchronization primitive explicitly designed for Swift’s concurrency model.

Why Choose Mutex?

The key value of Mutex lies in the fact that the compiler understands its concurrency semantics. It explicitly binds “mutability” to “concurrency safety,” ensuring that Sendable compliance is no longer dependent on developer discipline but is instead verified by the compiler.

Swift
import Synchronization

// 1. No need for @unchecked; the compiler automatically infers Sendable compliance.
final class SafeCache: Sendable {
    
    // 2. Mutex encapsulates the mutable state. 
    // The 'let' declaration ensures the reference to the Mutex itself is stable.
    private let storage = Mutex<[String: String]>([:])

    func update(key: String, value: String) {
        // 3. You must access the underlying data within the 'withLock' closure.
        // The compiler enforces safety within this critical section.
        storage.withLock { dict in
            dict[key] = value
        }
    }

    func value(for key: String) -> String? {
        // Synchronous APIs are preserved, removing the need for async/await.
        storage.withLock { dict in
            dict[key]
        }
    }
}

Key Considerations

  1. State Container Mechanism: Mutex is not just a traditional lock statement; it acts as a state container. Mutable state must be fully encapsulated within the generic Mutex wrapper. You must not leak pointers or references to the internal data outside the withLock closure.
  2. Keep Logic Simple: As with traditional locks, the closure passed to withLock should remain short and efficient. Avoid performing expensive operations or nested calls to prevent deadlocks and performance bottlenecks.
  3. Complementing Actors: Mutex does not replace Actors. It is a complementary tool best suited when you need synchronous APIs (e.g., inside computed properties), require ultra-low overhead, or need to integrate with legacy synchronous code.
  4. Performance: As part of the standard library, Mutex is highly optimized at the low level, offering extremely low overhead in non-contended states.

Further Reading

Related Tips

Subscribe to Fatbobman

Weekly Swift & SwiftUI highlights. Join developers.

Subscribe Now