Почему возникает дедлок при взаимной блокировке двух mutex в двух потоках?
Что проверяют
Знаете ли вы условия, при которых возникает взаимная блокировка, и владеете ли средствами стандартной библиотеки, которые её предотвращают. Два мьютекса это самый простой случай, на нём проверяют понимание, а не эрудицию.
Как отвечать
Дедлок требует одновременного выполнения четырёх условий: ресурс захватывается монопольно, поток удерживает один ресурс и ждёт второй, отобрать захваченное нельзя, ожидание замкнуто в цикл. Два потока, берущие пару мьютексов в противоположном порядке, дают ровно такой цикл: первый держит m1 и ждёт m2, второй держит m2 и ждёт m1.
Достаточно разорвать любое из условий, проще всего циклическое ожидание:
- захватывать оба мьютекса одной операцией через
std::scoped_lock. Внутри работает алгоритм с откатом, поэтому порядок аргументов значения не имеет; - если объединить нельзя, задать глобальный порядок захвата и соблюдать его во всём коде: по уровню подсистемы либо по адресу объекта;
- сокращать критическую секцию и не звать под захваченным мьютексом чужой код, который может взять свою блокировку.
void transfer(Account& a, Account& b, int sum) {
std::scoped_lock lock(a.m, b.m); // безопасно при любом порядке
a.balance -= sum;
b.balance += sum;
}Таймауты через std::timed_mutex и try_lock помогают обнаружить проблему и не повесить систему навсегда, но причину не устраняют.
Где спотыкаются
Предлагают std::recursive_mutex. Он решает другую задачу: повторный захват тем же потоком. К взаимной блокировке двух разных мьютексов отношения не имеет и обычно маскирует запутанную структуру вызовов.
Берут два std::lock_guard подряд и считают, что идиома захвата ресурса при создании защищает сама по себе. Она гарантирует освобождение при выходе и при исключении, но не порядок захвата: две строки подряд это и есть удержание с ожиданием.
Ищут проблему внутри одной функции. Цикл чаще складывается между модулями: метод под своим мьютексом вызывает колбэк или чужой объект, тот берёт свою блокировку, а обратный порядок встречается в соседнем файле. Классический пример это операция над двумя объектами одного класса, где каждый блокирует себя.