Ответ
В C++ для атомарной блокировки нескольких мьютексов с защитой от взаимоблокировок (deadlock) используются два основных механизма:
-
std::lock(C++11): Алгоритмически безопасно захватывает несколько мьютексов, используя deadlock-avoidance алгоритм (например, алгоритм обедающих философов). После захвата мьютексы передаются RAII-обёрткам с флагомstd::adopt_lock.std::mutex mtx1, mtx2; { std::lock(mtx1, mtx2); // Атомарный захват std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock); std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock); // Критическая секция защищена обоими мьютексами } // Мьютексы освобождаются автоматически -
std::scoped_lock(C++17): Более идиоматичная заменаstd::lockс variadic templates. Автоматически применяет deadlock-avoidance и обеспечивает RAII.std::mutex mtx1, mtx2; { std::scoped_lock lock(mtx1, mtx2); // Всё в одной строке // Критическая секция } // Автоматический unlock
Почему это важно? Последовательный захват (mtx1.lock(); mtx2.lock();) в разных потоках в обратном порядке — классическая причина deadlock. std::lock и std::scoped_lock гарантируют, что захват произойдёт атомарно для всей группы, исключая эту проблему.
Ответ 18+ 🔞
А, ну слушай, тут про мьютексы в плюсах, да? Просто пиздец как важно, если не хочешь, чтобы твоя многопоточка накрылась медным тазом в самый неподходящий момент.
Представь себе, два потока, как два упыря, хотят захватить два мьютекса. Один начинает с первого, потом ко второму тянется. А второй поток — хитрая жопа — начинает со второго, потом к первому лезет. И что получается? Первый ждёт второй, второй ждёт первый. Сидят, блядь, смотрят друг на друга, как дураки, и нихуя не делают. Это и есть deadlock, ёпта. Взаимная блокировка, ебать колотить. Программа просто встала колом, и хуй с горы её сдвинешь.
Так вот, чтобы не было этого цирка, умные дядьки придумали std::lock. Эта штука — она как спецназ. Ей говоришь: «Вот эти два (или больше) мьютекса, возьми их всех разом, но так, чтобы они не передрались». И она использует хитрый алгоритм (типа того, как философы за столом, чтобы все вилки одновременно схватить), и гарантированно захватывает их без deadlock'а. Но тут есть нюанс, блядь.
После std::lock мьютексы захвачены, но их надо ещё и корректно отпустить, когда работа закончится. Для этого используются RAII-обёртки, но с флагом std::adopt_lock. Это типа: «Чувак, я уже захвачен, просто следи за мной и отпусти в нужный момент».
std::mutex mtx1, mtx2;
{
std::lock(mtx1, mtx2); // Атомарный захват, без дури
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
// Сидишь тут в безопасности, как в танке
} // А тут они автоматом отпустятся — красота!
Но это, бля, как будто на дворе 2011-й год. С C++17 пришла красота — std::scoped_lock. Это вообще песня, я тебе скажу. Всё то же самое, но в одну строчку. Никаких этих флагов, никакой возни. Просто сказал, какие мьютексы нужны, и всё. Сам захватит безопасно, сам и отпустит. Удобно, ёбана!
std::mutex mtx1, mtx2;
{
std::scoped_lock lock(mtx1, mtx2); // Всё, пиши пропало. Захвачено.
// Делай что хочешь, тут рай
} // И память чистая, и совесть чистая
А почему это, блядь, так важно? Да потому что если ты будешь делать по-старинке, в лоб: mtx1.lock(); mtx2.lock(); в одном потоке, а в другом — mtx2.lock(); mtx1.lock();, то рано или поздно тебе будет пи**а. Программа зависнет, и будешь сидеть, чесать репу: «Ну какого хуя?». А это оно самое, classic deadlock, ебушки-воробушки.
Поэтому запомни, как «Отче наш»: хочешь несколько мьютексов — либо std::lock с adopt_lock, либо, что в разы лучше, std::scoped_lock. И будет тебе счастье, а не волнение ебать по ночам, почему сервер лег.