← Назад к вопросам

В чём разница между mutex и атомарными переменными?

2.2 Middle🔥 121 комментариев
#Многопоточность и синхронизация

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Разница между mutex и атомарными переменными

Оба механизма используются для синхронизации в многопоточном коде, но имеют кардинально разные подходы, производительность и области применения. Это критический выбор при проектировании конкурентных систем.

1. Базовые концепции

Mutex (Mutual Exclusion)

Mutex обеспечивает эксклюзивный доступ к ресурсу. Поток, который хочет использовать защищаемый ресурс, должен "захватить" мьютекс перед доступом.

#include <mutex>
#include <iostream>

int shared_counter = 0;
std::mutex counter_mutex;

void increment() {
    std::lock_guard<std::mutex> lock(counter_mutex);
    // Только один поток может быть здесь одновременно
    shared_counter++;
    // lock автоматически отпускается здесь (RAII)
}

Атомарные переменные

Атомарные переменные обеспечивают атомарные операции на уровне процессора. Операции над ними не могут быть прерваны и выполняются как целое.

#include <atomic>
#include <iostream>

std::atomic<int> shared_counter(0);

void increment() {
    // Операция атомарна на уровне процессора
    shared_counter++;
}

2. Механизм работы

Mutex — операционная система контролирует доступ

std::mutex m;

void worker() {
    m.lock();              // 1. Заблокировать мьютекс
    // Критическая секция   2. Только этот поток здесь
    m.unlock();            // 3. Разблокировать для других потоков
}

// Или с RAII (рекомендуется)
void worker_safe() {
    std::lock_guard<std::mutex> lock(m);  // Автоматический lock
    // Критическая секция
    // Автоматический unlock при выходе из scope
}

Что происходит под капотом:

  1. Если мьютекс свободен, поток захватывает его и продолжает
  2. Если занят, поток блокируется и переходит в режим ожидания
  3. Операционная система пробуждает поток при освобождении мьютекса

Атомарные переменные — процессор контролирует атомарность

std::atomic<int> counter(0);

void worker() {
    counter.store(5);      // Атомарная запись
    int val = counter.load();  // Атомарная чтение
    counter++;             // Атомарное увеличение
}

Что происходит под капотом:

  • На многопроцессорных системах используются инструкции типа CAS (Compare-and-Swap)
  • Процессор гарантирует, что операция завершится без прерывания
  • Нет переключения контекста, нет блокировки потоков

3. Производительность

Mutex — высокая стоимость при конфликтах

#include <mutex>
#include <thread>
#include <chrono>

int counter = 0;
std::mutex m;

void test_mutex() {
    for (int i = 0; i < 1000000; ++i) {
        std::lock_guard<std::mutex> lock(m);
        counter++;  // ~500-2000 ns за операцию
    }
}

int main() {
    auto start = std::chrono::high_resolution_clock::now();
    
    std::thread t1(test_mutex);
    std::thread t2(test_mutex);
    
    t1.join();
    t2.join();
    
    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    std::cout << "Time: " << duration.count() << " ms" << std::endl;
    // Результат: ~1000-2000 ms для 2 млн итераций
}

Атомарные переменные — низкая стоимость

#include <atomic>
#include <thread>
#include <chrono>

std::atomic<int> counter(0);

void test_atomic() {
    for (int i = 0; i < 1000000; ++i) {
        counter++;  // ~10-50 ns за операцию (зависит от процессора)
    }
}

int main() {
    auto start = std::chrono::high_resolution_clock::now();
    
    std::thread t1(test_atomic);
    std::thread t2(test_atomic);
    
    t1.join();
    t2.join();
    
    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    std::cout << "Time: " << duration.count() << " ms" << std::endl;
    // Результат: ~100-200 ms для 2 млн итераций
}

Производительность: Атомарные переменные в 10-100 раз быстрее!

4. Сравнительная таблица

АспектMutexAtomic
МеханизмОС контролирует доступПроцессор гарантирует атомарность
СтоимостьВысокая (500+ ns)Низкая (10-50 ns)
МасштабируемостьПлохая при высоком конфликтеХорошая для простых операций
Сложность защитыВся область в lockТолько атомарная операция
Блокировка потокаДа (ОС переключает контекст)Нет (busy-waiting или lock-free)
Подходит дляСложные критические секцииПростые счётчики, флаги

5. Практические примеры

Пример 1: Счётчик событий (Atomic)

std::atomic<int> event_count(0);

void worker_thread() {
    for (int i = 0; i < 1000; ++i) {
        // Обработка события
        event_count++;  // Быстро, без блокировки
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(worker_thread);
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    std::cout << "Total events: " << event_count << std::endl;  // 10000
}

Пример 2: Флаг завершения (Atomic)

std::atomic<bool> should_stop(false);

void worker_thread() {
    while (!should_stop.load()) {
        // Выполнять работу
        std::cout << ".";
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
    std::cout << "\nStopped" << std::endl;
}

int main() {
    std::thread t(worker_thread);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    should_stop.store(true);  // Остановить поток
    t.join();
}

Пример 3: Очередь задач (Mutex)

std::queue<Task> task_queue;
std::mutex queue_mutex;
std::condition_variable cv;

void worker_thread() {
    while (true) {
        std::unique_lock<std::mutex> lock(queue_mutex);
        
        // Ждать, пока в очереди появится задача
        cv.wait(lock, [] { return !task_queue.empty(); });
        
        if (task_queue.empty()) break;
        
        Task task = task_queue.front();
        task_queue.pop();
        lock.unlock();  // Разблокировать перед обработкой
        
        // Обработка задачи (долгая операция)
        process_task(task);
    }
}

void enqueue_task(const Task& task) {
    std::lock_guard<std::mutex> lock(queue_mutex);
    task_queue.push(task);
    cv.notify_one();  // Пробудить один рабочий поток
}

Пример 4: Lock-Free очередь с atomics (продвинутое)

template<typename T>
class LockFreeQueue {
private:
    struct Node {
        T value;
        std::atomic<Node*> next;
    };
    
    std::atomic<Node*> head;
    std::atomic<Node*> tail;
    
public:
    LockFreeQueue() {
        Node* dummy = new Node();
        head.store(dummy);
        tail.store(dummy);
    }
    
    void enqueue(const T& value) {
        Node* new_node = new Node();
        new_node->value = value;
        new_node->next.store(nullptr);
        
        Node* old_tail = tail.load();
        old_tail->next.store(new_node);
        tail.store(new_node);
    }
    
    bool dequeue(T& result) {
        Node* old_head = head.load();
        Node* new_head = old_head->next.load();
        
        if (new_head == nullptr) {
            return false;
        }
        
        result = new_head->value;
        head.store(new_head);
        delete old_head;
        return true;
    }
};

6. Когда использовать что

Используйте Mutex когда:

  • Защищаете сложную критическую секцию (несколько операций)
  • Нужна condition variable для синхронизации
  • Операция может быть долгой
  • Ресурс содержит несколько полей, которые должны быть согласованы
std::mutex db_mutex;
std::map<int, std::string> database;

void update_record(int id, const std::string& value) {
    std::lock_guard<std::mutex> lock(db_mutex);
    // Несколько операций, которые должны быть атомарны вместе
    database.erase(id);
    database.insert({id, value});
    log("Record updated");
}

Используйте Atomic когда:

  • Простая операция (чтение/запись, инкремент)
  • Очень часто используется (счётчики, флаги)
  • Нужна максимальная производительность
  • Нет condition variable
std::atomic<int> request_count(0);
std::atomic<bool> server_running(true);

void handle_request() {
    request_count++;  // Очень быстро
}

void shutdown() {
    server_running.store(false);  // Сигнал на остановку
}

7. Memory Ordering (продвинутое)

Атомарные переменные поддерживают разные модели консистентности памяти:

std::atomic<int> x(0);

// Самый строгий (полная synchronization)
x.store(5, std::memory_order_seq_cst);
int y = x.load(std::memory_order_seq_cst);

// Более слабые гарантии (выше производительность)
x.store(5, std::memory_order_release);
int y = x.load(std::memory_order_acquire);

// Самый слабый (никакой synchronization)
x.store(5, std::memory_order_relaxed);
int y = x.load(std::memory_order_relaxed);

Выводы

Mutex — универсальный инструмент:

  • Используйте по умолчанию при синхронизации потоков
  • Подходит для сложных критических секций
  • Есть condition_variable для координации

Atomic — высокопроизводительный инструмент:

  • Используйте для простых счётчиков и флагов
  • В 10-100 раз быстрее mutex
  • Требует понимания конкурентности

Правило: Для чистоты кода и простоты используйте mutex. Для производительности-критичного кода используйте atomic. В современном C++ правильно спроектированная система редко нуждается в явном использовании обоих одновременно.

В чём разница между mutex и атомарными переменными? | PrepBro