Как исправить дедлок, не задумываясь о порядке захвата mutex?
Что проверяют
Знаете ли вы средства стандартной библиотеки, которые снимают с программиста обязанность следить за порядком захвата. Следом обычно спрашивают, как std::lock избегает дедлока и что делать, если мьютексы захватываются в разных функциях.
Как отвечать
Главная мысль: захватывать все нужные мьютексы одной операцией.
std::scoped_lock(C++17):std::scoped_lock lk(m1, m2);захватывает несколько мьютексов без риска взаимной блокировки и отпускает их в деструкторе. Это современная замена связкеstd::lockплюсstd::lock_guardсstd::adopt_lock;std::lock(m1, m2): тот же алгоритм избегания дедлока. Стандарт не фиксирует реализацию, на практике это захват одного мьютекса иtry_lockостальных с откатом и повтором, если что-то занято;std::unique_lockсstd::defer_lock: когда захват нужно отложить или передать владение, а потом всё равно отдать вstd::lock;- один мьютекс вместо двух: если данные почти всегда меняются вместе, общий мьютекс проще и надёжнее;
- таймаут:
try_lock_forуstd::timed_mutex, при неудаче отпустить всё захваченное и повторить. Это запасной вариант, под нагрузкой он может дать livelock.
Дальше скажите о профилактике. Не вызывайте под мьютексом чужой код, колбэки и join(): так получают дедлок даже с одним мьютексом. Для поиска подойдут ThreadSanitizer (-fsanitize=thread), который сообщает о lock-order-inversion ещё до реального зависания, и Helgrind из Valgrind.
Где спотыкаются
Пишут std::unique_lock<std::mutex> lk(m, 100ms). У std::mutex нет захвата с таймаутом, такой код не скомпилируется, нужен std::timed_mutex.
Считают, что std::lock решает всё. Он помогает, только когда все мьютексы известны в одной точке. Если один захвачен в вызывающей функции, а второй глубоко в вызываемой, порядок снова на вашей совести.
Советуют std::recursive_mutex против самоблокировки или std::condition_variable «вместо мьютексов». Первое маскирует ошибку в дизайне, второе вообще работает только в паре с мьютексом.