TRT
Sign in
mediumAdvancedconcept

How does deadlock happen with multiple mutexes, and how do std::scoped_lock and lock hierarchies prevent it?

State the four Coffman conditions, trace opposite-order AB/BA acquisition, and compare std::scoped_lock deadlock-avoidance against global lock ordering.

TRT practice prompt — not a verified question from a named employer.

The Runtime Theory Team2 min read
Learn it firstDeadlock: What It Is and How std::lock Avoids ItPro — members onlyRecognize the four deadlock conditions, reproduce opposite-order locking, and fix it with std lock and scoped_lock.

A strong answer

A deadlock occurs when two or more threads freeze permanently because each is waiting for a resource held by another in a cycle. Specifically, all four Coffman conditions must hold simultaneously: mutual exclusion (locks are exclusive), hold and wait (a thread holds one mutex while waiting to acquire a second), no preemption (the OS will not forcibly steal a mutex from its owner), and circular wait (Thread A holds m1 and waits for m2, while Thread B holds m2 and waits for m1).

In C++, you break circular wait or hold-and-wait in two ways:

  1. Combined acquisition with std::scoped_lock (C++17) or std::lock (C++11): When both mutexes are visible at the same call site, std::scoped_lock lk(a.mtx, b.mtx); acquires both mutexes using a deadlock-avoidance algorithm (such as try-and-back-off or address-ordered locking) and releases both automatically via RAII when lk leaves scope—even if an exception is thrown.
  2. Global lock hierarchy (lock ordering): When locks are acquired across separate function layers where std::scoped_lock cannot see both mutexes at once, assign a strict total order—such as sorting mutexes by memory address std::less<std::mutex*>{} or assigning each layer a numeric hierarchy rank—and enforce that every thread acquires locks strictly in ascending order.

What the interviewer is listening for

  • Names the four deadlock conditions and identifies circular wait as the one we break in practice.
  • Shows std::scoped_lock lk(m1, m2); (or std::unique_lock with std::defer_lock + std::lock) and explains why naive sequential lock_guards on (a, b) and (b, a) deadlock.
  • Mentions if (&a == &b) return; before locking two objects (transfer(acc, acc)) so a thread does not deadlock with itself on a non-recursive std::mutex.

Common mistakes

  • Acquiring m1 in one function and calling a user callback or virtual function while still holding m1, which may acquire m2 in the opposite order.
  • Locking a.mtx and b.mtx without checking &a == &b first.
  • Using std::recursive_mutex as a band-aid for unclear ownership instead of fixing the locking design.

This answer walks

Practice follow-ups

  1. 01Why is std::scoped_lock(m1, m2) preferred over std::lock(m1, m2) with adopt_lock in C++17 and later?
  2. 02What can still cause a deadlock even when every multi-mutex lock call uses std::scoped_lock?

Not started

Sign in to save your learning progress.

Sign in to save