Какой механизм в C++ может атомарно заблокировать два мьютекса?

Ответ

В C++ для атомарной блокировки нескольких мьютексов с защитой от взаимоблокировок (deadlock) используются два основных механизма:

  1. 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);
        // Критическая секция защищена обоими мьютексами
    } // Мьютексы освобождаются автоматически
  2. 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. И будет тебе счастье, а не волнение ебать по ночам, почему сервер лег.