Give an example of a deadlock in which no participant holds a mutex, and explain what plays the role of the lock in it.
answer
- finite + exclusive + non-preemptible = acts like a lock
- pool permit, worker slot, queue slot, lease, row lock
- all capacity held by parties that cannot progress
- task waiting on a subtask queued behind it
- invisible to lock detectors; watch saturation metrics
basics
~20 sA task holding one pooled connection while needing a second one, in a pool where every connection is held by such a task. The pool permit is the lock: finite, exclusive, non-preemptible. Bounded queues, semaphores and worker slots behave the same.
solid answer
~60 sNothing in the four Coffman conditions mentions mutexes. Any resource that is **finite, exclusive and non-preemptible** can play the lock's role. Classic example: a connection pool of size N where each task borrows a connection and then, mid-work, needs a second one. Once N tasks each hold one, all N block forever waiting for a connection that only they could release. Circular wait over a multi-instance resource, no mutex anywhere. Other shapes worth naming: - **Thread-pool starvation deadlock**: a task running on a bounded pool blocks on the result of a subtask it submitted to the *same* pool; the subtask sits in the queue behind it. - **Bounded-queue deadlock**: stage A blocks pushing into a full queue toward B, while B blocks pushing into a full queue toward A. - **Message-passing deadlock**: two processes each blocked in a synchronous receive from the other. These are dangerous because automatic lock detectors do not see them: the symptom is saturation, and the diagnosis has to come from reasoning about who holds capacity.
code
text · 8 linespool capacity: 2
task1: borrow() -> c1 (free: 1)
task2: borrow() -> c2 (free: 0)
task1: borrow() ... blocks (needs a second connection)
task2: borrow() ... blocks (needs a second connection)
neither releases until it finishes; neither can finishgo deeper
Know one concrete example - a task holding one pooled connection while asking for a second - and that any limited exclusive resource can deadlock.
Generalize to worker slots and queue slots, and explain why enlarging the pool postpones rather than fixes it.
Diagnose from capacity accounting: who holds instances, whether holders are blocked, and which metrics would have made it visible; know the blind spot in automatic detectors.
Set the standing rules - one permit per unit of work, no blocking on work submitted to your own pool, separate pools for dependent stages, bounded waits with timeouts - and treat borrow depth as an architectural invariant.
## Locks are a special case, not the phenomenon Deadlock is defined over resources, not over mutexes. Re-reading the Coffman conditions with that in mind: mutual exclusion means the resource cannot be shared while in use; hold and wait means you can be holding one while asking for another; no preemption means nobody can take it back. A mutex satisfies all three, but so do many things a typical service uses constantly. ## Pool exhaustion with nested acquisition A pool of N database connections. A request handler borrows a connection, starts work, and then calls a helper that borrows a *second* connection - perhaps to read from a different data source, perhaps because a library opens its own. Under light load nothing happens, because free connections are always available. Under enough concurrency, N handlers each hold exactly one connection and each waits for a second. None will release until it finishes; none can finish. The pool is the resource type, the permit is the exclusive holding, and the wait is permanent. The hallmark that makes this a deadlock rather than mere saturation is that **all capacity is held by parties that cannot progress**. Saturation drains when slow work completes; this never drains. ## Thread-pool starvation deadlock A bounded worker pool where a running task submits a subtask to the same pool and blocks waiting for the subtask's result. The subtask is queued behind the running task, which will not release its worker until the subtask finishes. With one worker this deadlocks on the first attempt; with W workers it deadlocks as soon as W tasks do this simultaneously. Here the scarce resource is a **worker slot**. The same shape appears whenever a task waits on something that needs the pool that the task is occupying - which includes recursive parallel decomposition, and calling a service backed by the same executor. ## Bounded queues between stages A pipeline where stage A pushes to queue Q1 consumed by B, and B pushes to queue Q2 consumed by A (a feedback loop, or an error path). If both queues fill, A blocks on the full Q1 while holding its consumer role for Q2, and B blocks on the full Q2 while holding its role for Q1. The scarce resource is a **queue slot**. Unbounded queues remove this deadlock and replace it with unbounded memory growth, so the choice is between two failure modes rather than a free fix. ## Synchronous message passing Even with no shared memory at all, two processes that each perform a blocking send or receive toward the other with no buffering are deadlocked: each waits for a rendezvous the other will never reach. Message-passing designs remove data races, but circular waits survive; they simply become circular waits on messages. This is why request-response between two single-threaded actors, where each blocks awaiting the other's reply, deadlocks exactly like two mutexes taken in opposite orders. ## Cross-system and transactional variants - **Database row or range locks** held across two transactions updating the same rows in opposite orders. The engine usually detects and aborts a victim. - **Distributed leases and semaphores** in a coordination service, where the holder crashes or waits on a peer. - **Service A calls B, B calls A**, each with a bounded connection pool: pools on both sides fill with in-flight calls waiting on the other side. ## Why these are harder to diagnose Runtime deadlock detectors track only lock types they own. Pool permits, queue slots, worker slots and remote leases carry no ownership metadata the runtime can walk, so the automatic report says nothing and the dump looks like ordinary contention: many threads parked in a borrow or a put. The evidence you actually need is capacity accounting - how many instances exist, who holds them, and whether every holder is itself blocked. That argues for making these resources observable up front: metrics for pool saturation, wait time, borrow depth (how many permits one task can hold at once), and queue fullness turn an invisible deadlock into a visible one. ## The generalized rule Whenever a unit of work can hold one instance of a bounded resource while requesting another instance of the same or another bounded resource, you have reconstructed hold-and-wait over that resource, and a cycle becomes possible. Recognising that pattern - **nested acquisition of bounded capacity** - is the transferable skill; the specific resource is incidental.
- How do you tell pool-exhaustion deadlock apart from ordinary pool saturation?Look at whether the holders can finish. Under saturation the holders are actively doing work - running queries, awaiting I/O that will return - so the pool drains as they complete and wait times recover after the load spike. In a deadlock every holder is itself blocked requesting more capacity from the same exhausted resource, so free count stays at zero indefinitely, no borrow ever succeeds, and stacks of the holders show them parked in a borrow rather than in real work.
- Does switching to message passing instead of shared memory eliminate deadlock?It eliminates data races and safe-publication hazards, because state is owned by one participant and never mutated concurrently, but it does not eliminate deadlock. A blocking send or a wait for a reply is still a hold-and-wait on a non-preemptible resource, so two participants each awaiting the other's message form a circular wait exactly as two mutexes do. Avoiding it needs the same disciplines: no blocking waits inside a handler, bounded timeouts, or an ordering on who may request from whom.
saying these in an interview costs you the question
- Believing deadlock requires mutexes, so pools, queues and executors are assumed safe.
- Calling every exhausted pool a deadlock without checking whether the holders can still finish.
- Assuming a bigger pool fixes it - more capacity only delays a deadlock whose holders can never progress.
- Claiming message passing or actors make deadlock impossible.
- Trusting a runtime's automatic deadlock report to cover application-managed resources.