Какие плюсы и минусы у мьютекса (mutex) как примитива синхронизации в C++?

«Какие плюсы и минусы у мьютекса (mutex) как примитива синхронизации в C++?» — вопрос из категории Многопоточность, который задают на 25% собеседований C/C++ Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Плюсы мьютекса (std::mutex):

  • Гарантирует взаимное исключение (mutual exclusion). Это фундаментальный примитив для защиты критических секций, обеспечивающий, что только один поток в данный момент владеет блокировкой и имеет доступ к общим данным.
  • Простота и понятность модели использования. Базовый API (lock(), unlock()) легко понять. Использование RAII-обёрток, таких как std::lock_guard или std::unique_lock, делает код исключительно безопасным.
  • Интеграция с другими примитивами стандартной библиотеки. std::mutex напрямую работает с std::condition_variable для реализации шаблонов "ожидания по условию", что критически важно для многих многопоточных паттернов (например, producer-consumer).
  • Поддержка рекурсивных блокировок через std::recursive_mutex. Позволяет одному и тому же потоку захватывать один и тот же мьютекс несколько раз, что может упростить код в некоторых сценариях (например, при вызове рекурсивных функций).

Минусы мьютекса:

  • Относительно высокая стоимость операций. Если блокировка недоступна, поток переводится в состояние ожидания ядром ОС, что приводит к переключению контекста. Это дороже, чем спин-блокировка (std::atomic_flag), если время удержания блокировки очень мало.
  • Риск взаимной блокировки (deadlock). Возникает при неправильном порядке захвата нескольких мьютексов разными потоками. Для борьбы с этим следует использовать std::lock() для атомарного захвата нескольких мьютексов.
  • Отсутствие разделения на читателей и писателей. std::mutex не различает операции чтения и записи. Если данные часто читаются, но редко обновляются, более эффективным может быть std::shared_mutex (C++17), который позволяет множеству читателей работать одновременно.
  • Нельзя копировать или перемещать. Объект мьютекса должен быть статическим или разделяемым через ссылку/указатель.

Пример безопасного использования с RAII:

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

std::mutex g_console_mutex;
int g_shared_counter = 0;

void safe_increment() {
    for (int i = 0; i < 10000; ++i) {
        // std::lock_guard автоматически lock() в конструкторе и unlock() в деструкторе
        std::lock_guard<std::mutex> lock(g_console_mutex);
        ++g_shared_counter; // Критическая секция защищена
        // Выход из области видимости -> автоматический unlock
    }
}

int main() {
    std::thread t1(safe_increment);
    std::thread t2(safe_increment);
    t1.join();
    t2.join();
    std::cout << "Final counter: " << g_shared_counter << std::endl; // Всегда 20000
    return 0;
}