Взаимная блокировка и scoped_lock
Вернёмся на кухню. Чтобы нарезать хлеб, повару нужны два предмета: нож и разделочная доска. Первый повар взял нож и тянется за доской. Второй в ту же секунду взял доску и тянется за ножом. Каждый держит своё, ждёт чужое и не отдаёт своего — ждать они будут вечно.
С мьютексами бывает то же самое. Два потока захватывают два мьютекса в разном порядке:
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); // ✗
}Взаимная блокировка
Давайте разберём, что бывает, если один поток выполняет left, а другой одновременно right:
- Первый поток захватывает
a. - Второй поток захватывает
b. - Первый поток ждёт
b, которую держит второй. - Второй поток ждёт
a, которую держит первый.
Программа зависает навсегда. Такое взаимное ожидание называют взаимной блокировкой. Она случается не при каждом запуске: если первый поток успеет захватить оба мьютекса раньше, чем второй начнёт, всё пройдёт гладко. Поэтому её трудно найти, а на собеседовании о ней спрашивают часто.
Один порядок для всех
Первое средство — договориться о порядке. Если оба повара всегда берут сначала нож, а потом доску, тот, кто не успел взять нож, ждёт с пустыми руками и доску не держит. Для потоков это значит: все функции захватывают a раньше b. Если right тоже начнёт с a, взаимной блокировки не будет.
Здесь аналогия заканчивается: повара могут увидеть друг друга и договориться, а потоки — нет. Порядок захвата задаёт только тот, кто пишет программу, и новая функция может его нарушить.
scoped_lock
std::scoped_lock из заголовка <mutex> захватывает несколько мьютексов сразу и без взаимной блокировки, в каком бы порядке их ни передали:
#include <mutex>
std::mutex a;
std::mutex b;
void right() {
std::scoped_lock lock(b, a);
}Если один мьютекс уже занят, std::scoped_lock не держит второй, пока ждёт, поэтому повара не застревают с одним предметом в руках. Освобождает он оба мьютекса тоже в конце своей области видимости, как std::lock_guard.
Частая ошибка
Функция под мьютексом вызывает другую функцию, которая захватывает тот же мьютекс:
#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); // ✗
}Программа собирается без предупреждений. Но add пытается захватить a, который этот же поток уже держит. Для std::mutex это неопределённое поведение: например, поток может навсегда остаться ждать сам себя. Захватывайте мьютекс в одном месте: здесь add_twice может обойтись без своего guard и вызвать add дважды.
Главное
- Взаимная блокировка — два потока держат по мьютексу и ждут мьютекс друг друга; программа зависает навсегда.
- Она возникает, когда потоки захватывают мьютексы в разном порядке, и случается не при каждом запуске.
- Захватывайте мьютексы всегда в одном порядке или все сразу через
std::scoped_lock. - Повторный захват
std::mutexв потоке, который его уже держит, — неопределённое поведение.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.