NSLock, NSRecursiveLock, os_unfair_lock, Mutex, serial queue: how do you choose?

2.7.3 · L2 · Interview

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.

NSLockNSRecursiveLockOSAllocatedUnfairLockMutex
Re-entrantnoyesnono
Scoped withLockyesyesyesyes
Owns the datanonooptional (state)yes
Available fromalwaysalwaysiOS 16iOS 18

Common traps

  • Locking an NSLock twice 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… →