Skip to content
Theory · Multithreading: An Overview · lesson 6 of 12

Deadlock and scoped_lock

Back to the kitchen. To slice bread, a cook needs two things: a knife and a cutting board. The first cook has taken the knife and reaches for the board. At the same second, the second cook has taken the board and reaches for the knife. Each holds one thing, waits for the other and does not give theirs up, so they will wait forever.

The same happens with mutexes. Two threads lock two mutexes in opposite orders:

C++
std::mutex a;
std::mutex b;

void left() {
    std::lock_guard<std::mutex> ga(a);
    std::lock_guard<std::mutex> gb(b);
}

void right() {
    std::lock_guard<std::mutex> gb(b);
    std::lock_guard<std::mutex> ga(a);  // ✗
}

Deadlock

Let's see what happens if one thread runs left while another runs right at the same time:

  1. The first thread locks a.
  2. The second thread locks b.
  3. The first thread waits for b, which the second one holds.
  4. The second thread waits for a, which the first one holds.

The program hangs forever. Such mutual waiting is called a deadlock. It does not happen on every run: if the first thread manages to lock both mutexes before the second one starts, everything goes fine. That makes it hard to find, and interviewers like to ask about it.

One order for everyone

The first remedy is to agree on an order. If both cooks always take the knife first and the board second, the one who missed the knife waits empty-handed and does not hold the board. For threads, this means every function locks a before b. If right also starts with a, there is no deadlock.

This is where the analogy ends: cooks can see each other and agree, threads cannot. Only the person writing the program sets the locking order, and a new function can break it.

scoped_lock

std::scoped_lock from the <mutex> header locks several mutexes at once and without a deadlock, whatever order they are passed in:

C++
#include <mutex>

std::mutex a;
std::mutex b;

void right() {
    std::scoped_lock lock(b, a);
}

If one mutex is already taken, std::scoped_lock does not hold the other one while it waits, so the cooks never get stuck with one thing in hand. It releases both mutexes at the end of its scope, too, like std::lock_guard.

Common mistake

A function holding a mutex calls another function that locks the same mutex:

C++
#include <mutex>

std::mutex a;
int total = 0;

void add(int x) {
    std::lock_guard<std::mutex> guard(a);
    total += x;
}

void add_twice(int x) {
    std::lock_guard<std::mutex> guard(a);
    add(x);  // ✗
}

The program builds without warnings. But add tries to lock a, which the same thread already holds. For std::mutex that is undefined behavior: the thread may, for example, wait for itself forever. Lock the mutex in one place: here add_twice can do without its own guard and call add twice.

Key points

  • A deadlock is two threads each holding a mutex and waiting for the other's; the program hangs forever.
  • It happens when threads lock mutexes in different orders, and it does not show up on every run.
  • Always lock mutexes in one order, or all at once with std::scoped_lock.
  • Locking a std::mutex again in the thread that already holds it is undefined behavior.

Subscribe to take the course

The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.

Sign in another way