NSLock, NSRecursiveLock, os_unfair_lock, Mutex, serial queue: how do you choose?
Predict first
What does this print?
let l = NSLock()
print(l.try(), l.try())
- true true
- true false
- false false
Predict first
What does this print?
import Synchronization
let m = Mutex(0)
m.withLock { $0 += 1 }
print(m.withLock { $0 })
- 0
- 1
- Compile error
Mental model. A lock is a single-key bathroom: the first person in turns the key, everyone else waits. A recursive lock lets the same person pass the key to themselves.
All of them give mutual exclusion: one thread in the critical section at a time. They differ in re-entrancy, cost and API. NSLock is the simple default, but it is not re-entrant: locking it twice from the same thread deadlocks. NSRecursiveLock allows the same thread to lock again. os_unfair_lock is the fastest and has no fairness; use it through OSAllocatedUnfairLock (iOS 16), because the raw lock must not move in memory. Mutex (Swift 6, iOS 18, import Synchronization) is a noncopyable lock that owns the data it protects.
Always use a scoped call (withLock) or defer { unlock() } so an early return or throw cannot leave it locked. A serial DispatchQueue works too, but sync from the queue onto itself deadlocks. Prefer an actor for new async code, and keep lock-protected code short.
try() takes the lock if it is free and returns immediately. The second call finds it already held (by this same thread) and returns false.
A Mutex owns its value and only hands it out inside withLock, so you cannot touch it unprotected.
| NSLock | NSRecursiveLock | OSAllocatedUnfairLock | Mutex | |
|---|---|---|---|---|
| Re-entrant | no | yes | no | no |
Scoped withLock | yes | yes | yes | yes |
| Owns the data | no | no | optional (state) | yes |
| Available from | always | always | iOS 16 | iOS 18 |
Common traps
- Locking an
NSLocktwice on one thread. It waits for itself forever. - Calling unknown code while holding a lock. It may call back into you and deadlock, or take another lock in the opposite order.
They might follow up with
- What is priority inversion? A low-priority thread holds a lock a high-priority thread needs. Unfair locks donate priority to the owner to reduce it.
- How do you avoid lock-order deadlocks? Always take multiple locks in one fixed order, or avoid holding two at once.
Related questions
- 2.7.4 Why can’t you hold a lock across an await, and what does the compiler say?
- 2.7.5 Is Array thread-safe, and how do you make shared state safe?
← How would you design an in-memory image cache? Why can’t you hold a lock across an await, and what does the compiler… →