Что такое mutex в C++?

Ответ

std::mutex (mutual exclusion, взаимное исключение) — это базовый примитив синхронизации из стандартной библиотеки C++ (<mutex>), предназначенный для защиты общих данных от одновременного доступа нескольких потоков, предотвращая состояния гонки (race conditions).

Базовый пример (НЕ рекомендуется для production):

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

std::mutex g_console_mutex; // Мьютекс для защиты std::cout
int g_shared_counter = 0;
std::mutex g_counter_mutex;

void unsafe_increment() {
    // ПРОБЛЕМА: между проверкой и модификацией может вклиниться другой поток
    if (g_shared_counter < 100) {
        // Имитация работы
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
        ++g_shared_counter;
    }
}

void safe_increment() {
    // Правильный подход: блокируем мьютекс на ВСЁ время работы с разделяемой переменной
    std::lock_guard<std::mutex> lock(g_counter_mutex);
    if (g_shared_counter < 100) {
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
        ++g_shared_counter;

        // Защищённый вывод в консоль
        std::lock_guard<std::mutex> console_lock(g_console_mutex);
        std::cout << "Thread " << std::this_thread::get_id() 
                  << ": counter = " << g_shared_counter << std::endl;
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(safe_increment); // Используем безопасную версию
    }
    for (auto& t : threads) t.join();
    std::cout << "Final counter: " << g_shared_counter << std::endl; // Всегда <= 100
    return 0;
}

Ключевые классы для работы с мьютексами:

  1. std::lock_guard — простейшая RAII-обёртка. Захватывает мьютекс в конструкторе и освобождает в деструкторе. Нельзя разблокировать явно или повторно захватить.
  2. std::unique_lock — более гибкая обёртка. Позволяет:
    • Откладывать захват (std::defer_lock).
    • Явно вызывать lock()/unlock().
    • Работать с условными переменными (std::condition_variable).
    • Передавать владение мьютексом.

Пример с std::unique_lock и условной переменной:

std::mutex mtx;
std::condition_variable cv;
bool data_ready = false;

void producer() {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    {
        std::lock_guard<std::mutex> lock(mtx);
        data_ready = true;
    }
    cv.notify_one(); // Сигнализируем потребителю
}

void consumer() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return data_ready; }); // Освобождает мьютекс и ждёт notify
    // После пробуждения мьютекс снова захвачен
    std::cout << "Data is ready!" << std::endl;
}

Важные нюансы:

  • Взаимная блокировка (Deadlock): Возникает, когда два или более потока ждут друг друга. Лечится с помощью std::lock(), который захватывает несколько мьютексов атомарно.
  • Производительность: Мьютекс — «тяжёлый» примитив. Для простых атомарных операций над одним типом данных предпочтительнее использовать std::atomic.
  • Иерархия мьютексов (C++17): std::mutexstd::timed_mutex (с таймаутами) → std::recursive_mutex (повторно захватываемый тем же потоком) → std::shared_mutex (C++17, для режима «много читателей / один писатель»).

Ответ 18+ 🔞

А, ну вот, мой друг, подходи, садись на пенёк, слушай байку про мьютексы. Это ж, блядь, как в очереди за водкой в девяностые — если все сразу лезут, бутылёв не хватит, и всем пиздец. Так и с потоками: лезут они все разом к одной переменной, а она одна, хитрая жопа. Получается состояние гонки, или, как умные говорят, race condition. А итог — программа работает так, будто её пьяный ёжик писал.

Вот смотри, самый простой пример, который в продакшн совать — это как сунуть хуй в розетку. Опасно и больно.

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

std::mutex g_console_mutex; // Это наш охранник у двери в буфет
int g_shared_counter = 0;
std::mutex g_counter_mutex; // А это охранник у самой водки

void unsafe_increment() {
    // ПРОБЛЕМА: щас один поток проверит, что счётчик меньше 100, а пока он зевает, другой уже успеет туда впиздюрить и увеличить. Первый проснётся — а ему уже ёперный театр, он счётчик ломает.
    if (g_shared_counter < 100) {
        std::this_thread::sleep_for(std::chrono::milliseconds(10)); // Зевнул, блядь
        ++g_shared_counter;
    }
}

А вот как надо, по-человечески, с охраной:

void safe_increment() {
    // Берём нашего верного цепного пса — lock_guard. Он как только родился (конструктор), сразу мьютекс хватает. А как помрёт (деструктор) — отпускает. Удобно, ёпта.
    std::lock_guard<std::mutex> lock(g_counter_mutex);
    if (g_shared_counter < 100) {
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
        ++g_shared_counter;

        // А вывод в консоль — это отдельный буфет, ему свой охранник нужен, а то все строчки в кучу перемешаются.
        std::lock_guard<std::mutex> console_lock(g_console_mutex);
        std::cout << "Thread " << std::this_thread::get_id()
                  << ": counter = " << g_shared_counter << std::endl;
    }
}

Ну и главная функция, где мы этих потоков, как тараканов, запускаем:

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(safe_increment); // Запускаем безопасных ребят
    }
    for (auto& t : threads) t.join(); // Ждём, пока все отпьют
    std::cout << "Final counter: " << g_shared_counter << std::endl; // Всегда будет не больше 100, а не овердохуища.
    return 0;
}

А теперь про обёртки, это важно, как выбор между тупым топором и швейцарским ножом.

  1. std::lock_guard — это тупой, но надёжный топор. Взял в руки — уже рубишь. Из рук выронил (деструктор) — перестал. Разблокировать пораньше не получится, ядрёна вошь. Зато просто и дуракоустойчиво.

  2. std::unique_lock — это уже тот самый швейцарский нож с десятью лезвиями. Можешь взять, но не сразу открыть (defer_lock). Можешь открыть-закрыть (lock()/unlock()) когда захочешь. Идеален для условных переменных — это когда поток должен ждать, пока другой поток не крикнет "водку привезли!".

Вот, полюбуйся на магию ожидания:

std::mutex mtx;
std::condition_variable cv;
bool data_ready = false; // Флаг, что водка на столе

void producer() {
    std::this_thread::sleep_for(std::chrono::seconds(1)); // Идём за водкой
    {
        std::lock_guard<std::mutex> lock(mtx); // Зашли на кухню
        data_ready = true; // Поставили бутыль
    } // lock_guard помер, мьютекс отпустили
    cv.notify_one(); // Орём на весь дом: "ГОТОВО, ПИДАРАСЫ!"
}

void consumer() {
    std::unique_lock<std::mutex> lock(mtx); // Взяли нож в руки
    // А тут волшебство: wait() отпускает мьютекс и засыпает, не тратя процессорное время.
    // Ждёт, пока не придёт notify. А когда придёт — автоматически снова хватает мьютекс и проверяет условие.
    cv.wait(lock, []{ return data_ready; });
    // Условие true, мьютекс у нас в руках, можно действовать.
    std::cout << "Data is ready!" << std::endl; // Ура, пьём!
}

Ну и напоследок, лайфхаки, чтобы не обосраться:

  • Взаимная блокировка (Deadlock): Это когда два потока ждут друг друга, как два упрямых козла на мосту. Один держит мьютекс А и хочет Б, другой держит Б и хочет А. И стоят так до скончания времён. Лечится атомарным захватом через std::lock(mtx1, mtx2).
  • Производительность: Мьютекс — штука тяжёлая, как чугунный мост. Если тебе нужно просто атомарно увеличить счётчик, бери std::atomic — это как быстрый и точный укол, а не кувалда.
  • Иерархия: В C++17 мьютексы обзавелись роднёй. Есть timed_mutex — можно стучаться с таймаутом ("откроешь через 5 сек или пошёл нахуй"). recursive_mutex — позволяет одному потоку захватить себя несколько раз (осторожно, можно самого себя задушить). И король — shared_mutex:
    • Много читателей — могут заходить одновременно, как зрители в кино.
    • Один писатель — когда он пришёл, всех выгоняют и закрывают дверь. Идеально для данных, которые часто читают, но редко меняют.

Вот и вся философия. С мьютексом — как с последней пачкой сигарет в военкомате: или прячь и защищай, или будь готов, что тебе на всех не хватит.

Видео-ответы