skip to content

Explain what `Mutex.withLock { }` does mechanically, including how it behaves on exceptions and cancellation. How does it differ from manual `lock()`/`unlock()`?

level: middleimportance: must knowfreq 60%

answer

  1. withLock = lock + try { action } finally { unlock }
  2. inline → no alloc, non-local return works
  3. finally releases on throw AND cancellation
  4. manual lock()/unlock() can leak the lock on exception
  5. owner param → unlock verification + holdsLock

basics

~10 s

withLock acquires the lock, runs your block, and always releases it afterward — even if the block throws or the coroutine is cancelled. Manual lock()/unlock() makes you remember to release it yourself.

solid answer

~40 s

`withLock(owner) { action() }` is an **inline** extension. It conceptually does: `lock(owner)` (suspending until acquired), then `try { return action() } finally { unlock(owner) }`. The `finally` guarantees release on normal return, on a thrown exception, and on cancellation. Because acquiring suspends at a suspension point, a `CancellationException` can be thrown there; if cancelled *before* acquiring, the lock is never taken; if cancelled while holding, the `finally` releases it. With manual `lock()`/`unlock()` you must write the `try/finally` yourself, and forgetting it — or an exception between `lock` and `unlock` — leaks the lock forever, deadlocking every later waiter. The optional `owner` parameter lets `unlock` verify the right owner and powers `holdsLock`. Note `Mutex` is non-reentrant, so `withLock` inside `withLock` on the same mutex self-deadlocks.

code

kotlin · 14 lines
kotlin
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

val m = Mutex()

suspend fun safe() = m.withLock {
    error("boom")     // lock still released by the inline finally
}

// equivalent manual form
suspend fun manual() {
    m.lock()
    try { error("boom") } finally { m.unlock() }
}

go deeper

for a junior

Knows withLock auto-releases and is safer than manual lock/unlock.

for a middle

Can describe the lock + try/finally expansion and exception/cancellation safety.

for a senior

Explains the suspension-point cancellation semantics, the owner parameter, and when manual locking is justified.

for a principal

Reasons about FIFO fairness, contention costs, and where to avoid locks entirely via confinement/atomics in a larger design.

## What `withLock` expands to `withLock` is defined (roughly) as: ```kotlin public suspend inline fun <T> Mutex.withLock(owner: Any? = null, action: () -> T): T { lock(owner) try { return action() } finally { unlock(owner) } } ``` Because it's `inline`, there's no lambda allocation and `return`/non-local control flow works naturally. ## Step by step 1. **`lock(owner)`** — a `suspend` call. If the mutex is free it's taken immediately; if held, the coroutine **suspends** (frees its thread) and is resumed FIFO when released. 2. **`action()`** — your critical section runs while you hold the lock. 3. **`finally { unlock(owner) }`** — runs no matter how the block exits. ## Exception & cancellation behavior - **Exception in the block** → propagates, but `finally` releases the lock first. No leak. - **Cancellation while waiting in `lock()`** → `lock()` throws `CancellationException` at its suspension point; the lock was never acquired, so nothing to release. - **Cancellation while holding** → the `finally` still runs and releases. This exception/cancellation safety is the main reason to prefer `withLock` over manual control: ```kotlin // Manual — error-prone mutex.lock() try { risky() // if you forget try/finally, a throw here leaks the lock forever } finally { mutex.unlock() } ``` ## The `owner` parameter `lock(owner)` / `unlock(owner)` / `holdsLock(owner)` let you tag who holds the lock. `unlock` with the wrong owner throws `IllegalStateException`, catching unbalanced lock/unlock bugs. `owner` is also used to make some helper checks possible. ## Non-reentrancy reminder ```kotlin mutex.withLock { mutex.withLock { /* self-deadlock: suspends forever */ } } ``` Design critical sections so they never re-acquire the same mutex. ## Summary `withLock` = `lock` + `try/finally unlock`, inline, safe under exceptions and cancellation. Manual `lock()/unlock()` is only justified when you genuinely need to acquire and release across non-lexical boundaries — and then you own the `try/finally`.

  • What happens if a coroutine is cancelled while suspended inside `lock()`?
    `lock()` throws `CancellationException` at the suspension point and the lock is never acquired, so no `unlock` is needed and no leak occurs.
  • When is manual `lock()`/`unlock()` actually justified?
    When acquire and release must straddle non-lexical scopes (e.g. acquire in one method, release in a callback). Then you take ownership of the `try/finally` yourself.

saying these in an interview costs you the question

  • Believing `withLock` leaks the lock on exception
  • Not putting `unlock` in a `finally` when using manual locking
  • Thinking cancellation while waiting leaves the lock held
  • Calling `withLock` recursively on the same mutex
  • Not knowing `withLock` is inline

context