В чём разница между mutex и атомарными переменными (std::atomic) в C++?

Ответ

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

std::atomic<T> — это шаблонный тип, обеспечивающий безблокирующую (lock-free или wait-free) синхронизацию для отдельных операций над переменной. Атомарные операции гарантированно выполняются как единое целое с точки зрения других потоков.

Ключевые отличия:

  1. Гранулярность: Mutex защищает произвольный блок кода (критическую секцию). std::atomic защищает доступ к одной конкретной переменной.
  2. Производительность: Атомарные операции, как правило, значительно быстрее, так как реализуются с помощью атомарных инструкций процессора (например, LOCK CMPXCHG на x86) и не требуют переключения контекста. Mutex involves system calls and context switches when contention occurs.
  3. Применимость: Mutex можно использовать для синхронизации доступа к сложным структурам данных или для выполнения последовательности операций (транзакции). std::atomic подходит для простых операций: чтение/запись, инкремент, сравнение с обменом (CAS).

Пример: потокобезопасный счётчик.

С использованием std::mutex (избыточно для одной операции):

#include <mutex>

class CounterMutex {
    int value = 0;
    mutable std::mutex mtx;
public:
    void increment() {
        std::lock_guard<std::mutex> lock(mtx); // Блокировка
        ++value; // Неатомарная операция под защитой мьютекса
    } // Автоматическая разблокировка
    int get() const {
        std::lock_guard<std::mutex> lock(mtx);
        return value;
    }
};

С использованием std::atomic (оптимально):

#include <atomic>

class CounterAtomic {
    std::atomic<int> value{0};
public:
    void increment() {
        // Атомарная операция fetch_add. Никаких блокировок.
        value.fetch_add(1, std::memory_order_relaxed);
    }
    int get() const {
        // Атомарное чтение.
        return value.load(std::memory_order_acquire);
    }
};

Пример, где mutex необходим, а atomic недостаточен:

// Нужно атомарно увеличить два счётчика.
// С atomic это две отдельные операции, между которыми другой поток может вмешаться.
std::atomic<int> cnt1{0}, cnt2{0};
// НЕПРАВИЛЬНО: состояние (cnt1, cnt2) может быть несогласованным для внешнего наблюдателя.
cnt1++;
cnt2++; 

// ПРАВИЛЬНО: используем mutex для атомарности составной операции.
std::mutex mtx;
mtx.lock();
cnt1++;
cnt2++;
mtx.unlock();

Правило выбора: Используйте std::atomic для простых операций над одиночными переменными (флаги, счётчики, указатели). Используйте std::mutex (или другие мьютексы) когда нужно защитить доступ к нескольким переменным одновременно или выполнить последовательность операций как единое целое.

Ответ 18+ 🔞

Ну вот, опять про мьютексы и атомики спрашивают. Давай разжую, как для дебила, только ты не обижайся — я так со всеми. Представь, что у тебя есть общий унитаз в общаге. std::mutex — это как дверь с замком. Зашёл, закрылся на щеколду, сделал свои грязные делишки, вышел — другой может зайти. Пока ты внутри, все остальные стоят и ждут, как лохи. Это и есть блокирующая синхронизация — поток тупо спит, пока его не пустят.

А вот std::atomic<T> — это уже не дверь, а какая-то хитрая жопа, типа писсуара с автоматическим смывом. Подошёл, пописал, смылось — и всё это так быстро и чётко происходит, что даже если рядом другой чувак стоит, вы не мешаете друг другу. Никто никого не ждёт, безблокирующая синхронизация, ёпта. Операция над переменной (типа инкремент счётчика) происходит за один такт, и для других потоков она выглядит как мгновенная — либо сделано, либо нет.

Так в чём, блядь, разница, спросишь ты?

  1. Масштаб бардака. Мьютекс — это как взять в охапку всю комнату и сказать: «Моё!». Атомик — это только на один тапок наступить: «Этот тапок мой, а остальное — как хотите».
  2. Скорость. Атомарные операции — это просто космос по скорости, потому что процессор сам их поддерживает на уровне инструкций. А мьютекс — это уже поход в ядро ОС, переключение контекста, очередь… Короче, овердохуища накладных расходов, если драка идёт.
  3. Когда что применять. Нужно безопасно увеличить один счётчик в двадцати потоках? std::atomic<int> — твой выбор, да похуй на мьютексы. А вот если тебе нужно, условно, сначала снять деньги с одного счёта, а потом положить на другой, и чтобы никто в промежутке не увидел, что деньги в воздухе — тут без мьютекса хуй с горы. Атомиками такую пару операций не связать.

Смотри, как счётчик делать.

Вариант через мьютекс (работает, но для счётчика — как из пушки по воробьям):

#include <mutex>

class CounterMutex {
    int value = 0;
    mutable std::mutex mtx; // Наш замок на двери
public:
    void increment() {
        std::lock_guard<std::mutex> lock(mtx); // Закрылся, все ждут
        ++value; // Спокойно плюсуем
    } // Вышел — щеколда сама открылась
    int get() const {
        std::lock_guard<std::mutex> lock(mtx);
        return value;
    }
};

А теперь вариант для адекватных людей, через atomic:

#include <atomic>

class CounterAtomic {
    std::atomic<int> value{0}; // Объявил и всё, больше ни о чём не парись
public:
    void increment() {
        // Никаких блокировок! Просто атомарно прибавили.
        value.fetch_add(1, std::memory_order_relaxed);
    }
    int get() const {
        // Атомарно прочитали.
        return value.load(std::memory_order_acquire);
    }
};

А вот ситуация, где atomic нихуя не спасёт, и нужен именно mutex:

// Допустим, тебе надо увеличить ДВА счётчика так, чтобы со стороны это выглядело как одна операция.
// С atomic это две отдельные команды.
std::atomic<int> cnt1{0}, cnt2{0};
// НЕПРАВИЛЬНО: другой поток между этими строчками может влезть и увидеть, что cnt1 уже ++, а cnt2 ещё нет.
// Состояние ебаное, неконсистентное.
cnt1++;
cnt2++;

// ПРАВИЛЬНО: берём мьютекс и делаем всё под его крышей.
std::mutex mtx;
std::lock_guard<std::mutex> lock(mtx); // Захватили
cnt1++; // Уже не атомики, но нам похуй — они под защитой мьютекса
cnt2++;

Итоговое правило, чтобы не быть распиздяем: Если твоя «критическая секция» — это одна операция над одной переменной (++ , запись, чтение, compare-and-swap), бери std::atomic и не парься. Если тебе нужно сделать несколько действий, чтобы система в целом перешла из одного согласованного состояния в другое — тогда std::mutex (или другие замки). Всё, вопрос закрыт, иди на хуй.