Why can’t you hold a lock across an await, and what does the compiler say?

2.7.4 · L3 · Interview

Predict first

Does this compile in Swift 6 language mode?

func work() async {
    let l = NSLock()
    l.lock()
    l.unlock()
}
  • Yes
  • No

Mental model. Taking the bathroom key and leaving the building for lunch. Everyone else is stuck, and you might return through a different door.

A lock is owned by a thread. A task that suspends at an await may resume on a different thread, so unlocking afterwards can be the wrong thread, and in the meantime the lock stays held while other tasks pile up behind it. That is a recipe for deadlock and stalls.

Swift 6 enforces the rule: lock.lock() and unlock() are unavailable from asynchronous contexts. Use a scoped call whose closure is synchronous (withLock { … }, or Mutex.withLock), do the await before or after it, and never inside. If the work genuinely needs to suspend while owning state, make that state an actor.

The compiler rejects manual lock() and unlock() in an async function and asks for scoped locking instead.

Common traps

  • Locking, awaiting, then unlocking. The compiler now refuses; older code did it and deadlocked.
  • Doing slow work inside withLock. Copy what you need out, release, then work.

They might follow up with

  • What is actor reentrancy? At an await inside an actor, other calls can run, so state may change across it. Re-check after resuming.
  • How do you migrate old lock code? Replace manual lock/unlock with withLock and keep the body non-async.

Related questions

  • 2.7.3 NSLock, NSRecursiveLock, os_unfair_lock, Mutex, serial queue: how do you choose?
  • 2.7.5 Is Array thread-safe, and how do you make shared state safe?

← NSLock, NSRecursiveLock, os_unfair_lock, Mutex, serial queue: how do…   Is Array thread-safe, and how do you make shared state safe? →