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

«Что такое mutex в C++?» — вопрос из категории Многопоточность, который задают на 30% собеседований C/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, для режима «много читателей / один писатель»).

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