Explain what `Mutex.withLock { }` does mechanically, including how it behaves on exceptions and cancellation. How does it differ from manual `lock()`/`unlock()`?
answer
- withLock = lock + try { action } finally { unlock }
- inline → no alloc, non-local return works
- finally releases on throw AND cancellation
- manual lock()/unlock() can leak the lock on exception
- owner param → unlock verification + holdsLock
basics
~10 swithLock 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 linesimport 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
Knows withLock auto-releases and is safer than manual lock/unlock.
Can describe the lock + try/finally expansion and exception/cancellation safety.
Explains the suspension-point cancellation semantics, the owner parameter, and when manual locking is justified.
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