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

Что такое race condition и как его избежать?

1.3 Junior🔥 211 комментариев
#Linux и операционные системы#Многопоточность и синхронизация#Язык C++

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

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

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

Race Condition и способы его избежания

Race condition (условие гонки) — это ошибка синхронизации, когда результат программы зависит от временного порядка выполнения потоков. Несколько потоков одновременно обращаются к общему ресурсу, и их операции перемешиваются, приводя к непредсказуемому результату. Это один из самых опасных багов в многопоточных программах, так как он недетерминирован и сложно воспроизводится.

Пример Race Condition

#include <thread>
#include <iostream>

int counter = 0;  // Общий ресурс

void increment() {
    // counter++ состоит из 3 операций
    for (int i = 0; i < 100000; i++) {
        counter++;  // Непотокобезопасно!
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    
    t1.join();
    t2.join();
    
    // Ожидаем: 200000, На деле: произвольное число!
    std::cout << "Counter: " << counter << std::endl;
    return 0;
}

1. Решение: Mutex (Взаимное исключение)

#include <thread>
#include <mutex>

int counter = 0;
std::mutex mtx;

void increment() {
    for (int i = 0; i < 100000; i++) {
        std::lock_guard<std::mutex> lock(mtx);
        counter++;  // Теперь потокобезопасно
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    
    t1.join();
    t2.join();
    
    std::cout << "Counter: " << counter << std::endl;  // Гарантированно 200000
    return 0;
}

2. Решение: std::atomic (Атомарные операции)

#include <thread>
#include <atomic>

std::atomic<int> counter(0);  // Атомарная переменная

void increment() {
    for (int i = 0; i < 100000; i++) {
        counter++;  // Атомарно! Нет race condition
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    
    t1.join();
    t2.join();
    
    std::cout << "Counter: " << counter << std::endl;  // 200000
    return 0;
}

3. Решение: Read-Write Lock (Shared Mutex)

#include <shared_mutex>

int data = 0;
std::shared_mutex data_mutex;

void reader(int id) {
    std::shared_lock<std::shared_mutex> lock(data_mutex);
    std::cout << "Read: " << data << std::endl;
}

void writer() {
    std::unique_lock<std::shared_mutex> lock(data_mutex);
    data++;
}

4. Решение: Thread-Safe классы

class ThreadSafeCounter {
private:
    int counter = 0;
    mutable std::mutex mtx;
    
public:
    void increment() {
        std::lock_guard<std::mutex> lock(mtx);
        counter++;
    }
    
    int getValue() const {
        std::lock_guard<std::mutex> lock(mtx);
        return counter;
    }
};

Лучшие практики избежания Race Conditions

1. Минимизируй критическую секцию

// Плохо: держим lock долго
std::lock_guard<std::mutex> lock(mtx);
data.process();  // Дорогая операция

// Хорошо: держим lock только необходимо
T localData;
{
    std::lock_guard<std::mutex> lock(mtx);
    localData = data;
}
localData.process();  // Без lock

2. Избегай deadlock'а

// Неправильно: разные порядки блокировок
void thread1() {
    std::lock_guard<std::mutex> l1(m1);
    std::lock_guard<std::mutex> l2(m2);
}

void thread2() {
    std::lock_guard<std::mutex> l2(m2);  // Другой порядок — DEADLOCK!
    std::lock_guard<std::mutex> l1(m1);
}

// Правильно: одинаковый порядок или std::lock
std::lock(m1, m2);

3. Используй более высокоуровневые конструкции

// Promise/Future для синхронизации
#include <future>

std::promise<int> prom;
std::thread t([&prom]() {
    prom.set_value(42);
});

int result = prom.get_future().get();

4. Thread-sanitizer для обнаружения

g++ -fsanitize=thread -g program.cpp
./a.out  # Найдёт race conditions во время выполнения

Таблица синхронизации

ИнструментСкоростьСложностьКогда использовать
std::atomicОчень быстроПростаяСчётчики, флаги
std::mutexБыстроСредняяКритические секции
std::shared_mutexБыстроСложнаяМного читателей
Lock-freeОчень быстроОчень сложнаяЭкстремальная нагрузка

Заключение

Race condition можно избежать:

  1. std::atomic — для простых операций (самый быстрый)
  2. std::mutex — для критических секций
  3. std::shared_mutex — для reader-writer проблем
  4. Всегда минимизируй критические секции и соблюдай одинаковый порядок блокировок
  5. Тестируй с thread-sanitizer для обнаружения скрытых ошибок
  6. Используй высокоуровневые конструкции (Promise/Future вместо ручной синхронизации)