Race condition возникает, когда два или более потока или процесса одновременно обращаются к общим данным, и хотя бы один из них выполняет запись. Результат зависит от точного порядка выполнения, который определяет планировщик ОС — непредсказуемо. Это один из наиболее сложно воспроизводимых классов ошибок: баг проявляется редко, часто пропадает при добавлении отладочного кода (Heisenbug).
Как работает
Классический пример: два потока одновременно читают значение счётчика (counter = 5), оба увеличивают его на 1, оба записывают. Ожидаемый результат — 7. Реальный — 6, потому что оба прочитали 5, не увидев записи друг друга. Операция counter++ в большинстве языков не атомарна: это три операции ЦП (read, increment, write).
Методы синхронизации для предотвращения race condition:
- Mutex (мьютекс) — взаимное исключение. Только один поток может удерживать мьютекс одновременно.
pthread_mutex_lock()в C,synchronizedв Java,threading.Lock()в Python. - Semaphore (семафор) — обобщённый мьютекс с счётчиком: позволяет N потокам одновременно. Используется для ограничения числа параллельных операций.
- Atomic операции — аппаратно атомарные операции чтения-изменения-записи:
std::atomicв C++,java.util.concurrent.atomic,sync/atomicв Go. Быстрее мьютексов для простых счётчиков. - Immutability (неизменяемость) — если данные неизменны после создания, race condition невозможна. Функциональный подход.
- Lock-free структуры данных — Compare-And-Swap (CAS) операции без блокировок: очереди, стеки без мьютексов.
Инструменты обнаружения race condition: ThreadSanitizer (TSan) для C/C++/Go — компилятор вставляет инструментацию, которая обнаруживает гонки в runtime; Helgrind (Valgrind); Java Concurrency Stress (jcstress).
История
Термин "race condition" появился в теории электронных схем в 1950-х годах (Edsger Dijkstra упоминает концепцию в работах 1965 года). В 1965 году Dijkstra сформулировал проблему взаимного исключения и предложил семафоры как решение. Алгоритм Петерсона (1981) решает задачу взаимного исключения для двух потоков без аппаратной поддержки. С ростом многоядерных процессоров (2005+) race conditions стали системной проблемой серверной разработки.
На что обращать внимание
В веб-разработке race condition встречается в: параллельных запросах к БД без транзакций (двойное списание, дублирование заказа), кэшировании при Cache stampede (многие запросы одновременно промахиваются и идут к БД), очередях задач без атомарных операций захвата. На уровне хостинга: PHP-FPM с несколькими worker-процессами, использующими общий файл-семафор или запись в файл без блокировки.
История изучения Race Condition
Термин «race condition» введён в технической литературе в 1954 году в контексте аналоговых электронных схем. В программировании формализован Дейкстрой при разработке алгоритмов взаимного исключения (mutex) в 1965 году. Семафоры — механизм синхронизации, описанный Дейкстрой в 1968 году. Helgrind (Valgrind) — инструмент обнаружения гонок в C/C++ появился в 2002 году. Go Race Detector встроен в компилятор Go с 2012 года.
Классические примеры Race Condition
# PHP пример без защиты — race condition
$count = get_counter_from_db(); // читаем = 100
$count++; // увеличиваем
save_counter_to_db($count); // сохраняем = 101
# Два одновременных запроса оба прочтут 100, оба запишут 101
# Правильно — атомарная операция в Redis
$redis->incr('counter'); // атомарно
Как предотвратить Race Condition на хостинге
| Механизм | Применение |
|---|---|
| Mutex/Lock | один процесс в PHP |
| Redis SETNX / Redlock | распределённая блокировка |
| Database transactions | атомарные операции в MySQL/PostgreSQL |
| SELECT FOR UPDATE | блокировка строки в БД |
| Atomic operations | Redis INCR, PostgreSQL SEQUENCE |
Типичные ошибки
- Чтение-изменение-запись без транзакций в многопроцессной среде PHP-FPM.
- Использование файловых блокировок (
flock()) для распределённых VPS: работает только на одном сервере. - Deadlock при неправильном порядке получения блокировок: A ждёт B, B ждёт A.
- Игнорирование race condition в тестах: ошибки проявляются только при высокой конкурентности.