В чём разница между mutex и condition variable в контексте многопоточного программирования на C++?

Ответ

std::mutex — это примитив синхронизации, обеспечивающий взаимное исключение (mutual exclusion). Его единственная цель — гарантировать, что в один момент времени критическую секцию кода выполняет не более одного потока. Он не предоставляет механизма для ожидания какого-либо условия.

std::condition_variable — это механизм оповещения между потоками. Он позволяет одному или нескольким потокам блокироваться в ожидании выполнения некоторого условия, связанного с общими данными. Ключевой момент: condition_variable всегда работает в паре с mutex.

Простая аналогия: Mutex — это ключ от комнаты (только один человек может войти). Condition Variable — это звонок будильника, который говорит ждущему снаружи человеку: "Теперь можно проверить, свободна ли комната".

Типичный шаблон использования condition variable в C++:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>

std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue;
bool finished = false;

// Поток-производитель
void producer() {
    for (int i = 0; i < 10; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        {
            std::lock_guard<std::mutex> lock(mtx);
            data_queue.push(i);
            std::cout << "Produced: " << i << std::endl;
        } // lock_guard выходит из области видимости, мьютекс освобождается
        cv.notify_one(); // Оповещаем одного потребителя
    }
    {
        std::lock_guard<std::mutex> lock(mtx);
        finished = true;
    }
    cv.notify_all(); // Оповещаем всех потребителей о завершении
}

// Поток-потребитель
void consumer(int id) {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        // 1. Освобождаем мьютекс и ждём сигнала.
        // 2. При получении сигнала снова захватываем мьютекс.
        // 3. Проверяем условие: если очередь не пуста ИЛИ работа завершена, выходим из ожидания.
        cv.wait(lock, [] { 
            return !data_queue.empty() || finished; 
        });

        // Если работа завершена и очередь пуста — выходим.
        if (finished && data_queue.empty()) {
            break;
        }

        // Извлекаем данные из очереди.
        int value = data_queue.front();
        data_queue.pop();
        lock.unlock(); // Явно отпускаем мьютекс перед долгой обработкой.

        std::cout << "Consumer " << id << " processed: " << value << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
}

int main() {
    std::thread prod(producer);
    std::thread cons1(consumer, 1);
    std::thread cons2(consumer, 2);

    prod.join();
    cons1.join();
    cons2.join();
    return 0;
}

Итоговое сравнение:

  • Mutex решает проблему конкуренции (race condition).
  • Condition Variable решает проблему координации (coordination/signaling), позволяя потокам эффективно ждать событий, не занимая CPU (busy-wait).

Ответ 18+ 🔞

А, слушай, вот объясняю на пальцах, чтобы даже мартышлюшка поняла. std::mutex — это как ключ от сортира на вокзале. Один ключ, одна дырка. Хочешь зайти — бери ключ, делай свои дела, потом выходи и вешай ключ обратно. Если ключа нет — сиди и жди, пока какой-нибудь пидарас шерстяной не выйдет. Всё просто: захватил мьютекс — работай, отпустил — иди нахуй, пусть другие работают. Он решает проблему, когда два потока лезут в одну переменную как олени, и получается пиздопроебибна.

А вот std::condition_variable — это уже другая, хитрая жопа. Это не ключ, а такой звонок-будильник. Представь: ты пришёл в эту самую сортирную кабинку, а там нихуя нет — бумаги, например. И что, будешь тупо стоять и пялиться в стену, пока уборщица не придёт? Нет, ты такой: "Ладно, я пошел покурить, позвони мне, когда бумага появится". Вот cv.wait() — это и есть "пошел покурить". Ты отпускаешь мьютекс (ключ вешаешь обратно, чтобы другие могли зайти и, возможно, бумагу положить), и твой поток засыпает, не жрёт процессорное время.

А потом, когда какой-то добрый самаритянин (другой поток) бумагу таки положит, он крикнет cv.notify_one() — типа "эй, чувак, бумага есть!". Твой поток просыпается, снова хватает мьютекс (проверяет, свободна ли кабинка), и только потом заходит и делает свои дела. И тут главный фокус, ёпта: когда ты просыпаешься, ты обязательно проверяешь условие заново. Вдруг это не твой звонок был, или бумагу уже кто-то сгрёб? Поэтому wait почти всегда используют с лямбдой, которая проверяет это условие.

Смотри на примере, тут всё как в жизни:

std::mutex mtx; // Ключ от сортира
std::condition_variable cv; // Звонок уборщице
std::queue<int> tasks; // Та самая бумага (или её отсутствие)
bool work_done = false; // Флажок, что всё, пиздец, уборщица уволилась

// Поток-начальник (производитель)
void boss() {
    for(int i = 0; i < 5; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Прикидывается занятым
        {
            std::lock_guard<std::mutex> lock(mtx); // Взял ключ, зашёл в кабинку
            tasks.push(i); // Положил задание (бумагу)
            std::cout << "Задание " << i << " выдал!n";
        } // Ключ сам вернулся на место (деструктор lock_guard)
        cv.notify_one(); // Крикнул работяге: "Задание есть, иди работай!"
    }
    // Всё, задания кончились
    {
        std::lock_guard<std::mutex> lock(mtx);
        work_done = true; // Вывесил табличку "ЗАКРЫТО НА САНИТАРНЫЙ ДЕНЬ"
    }
    cv.notify_all(); // Орет на всех работяг: "Расходимся, пиздуй домой!"
}

// Поток-работяга (потребитель)
void worker(int id) {
    while(true) {
        std::unique_lock<std::mutex> lock(mtx); // Взял ключ

        // Магия тут! Ждём звонка, но с проверкой.
        // Лямбда — это наша проверка: "А точно ли есть что делать?"
        cv.wait(lock, [&]() {
            // Возвращаем true, если либо задание есть, либо всё кончено.
            return !tasks.empty() || work_done;
        });

        // Сюда попадаем только если: 1) нас разбудили, и 2) условие true.
        // Если работа закончена и заданий нет — сваливаем.
        if(work_done && tasks.empty()) {
            break;
        }

        // Ага, задание есть! Забираем.
        int task = tasks.front();
        tasks.pop();

        lock.unlock(); // Сразу отпускаем мьютекс! Не будем свиньями, пусть другие работают.

        // И долго и упорно пашем.
        std::cout << "Работяга " << id << " пашет над заданием " << task << "n";
        std::this_thread::sleep_for(std::chrono::milliseconds(250));
    }
}

Итог, блядь, простой:

  • mutex — это чтобы не дать двум потокам влезть в одно место одновременно. Решает проблему гонки данных (race condition). Без него будет овердохуища проблем.
  • condition_variable — это чтобы поток мог эффективно ждать события, не тратя CPU впустую. Решает проблему координации. Без неё пришлось бы делать while(!condition) { sleep(); }, что и медленно, и некрасиво.

Запомни, как "Отче наш": мьютекс защищает данные, а условная переменная позволяет ждать, связав это ожидание с изменением этих самых данных. Всё, теперь ты гуру многопоточности, можешь идти и ебашить.