# Состояние гонки и блокировки

> Ошибка, когда результат зависит от непредсказуемого порядка выполнения потоков, и средства синхронизации против неё — мьютексы, атомарные операции, каналы; а также взаимоблокировки, которые эти средства порождают.

Источник: https://llmdocs.ru/wiki/race-conditions-locks/ · обновлено 2026-11-09 · лицензия CC BY-SA 4.0

- **Гонка (race condition)**: результат зависит от порядка или времени выполнения
- **Гонка данных (data race)**: одновременный доступ к памяти, хотя бы один — запись, без синхронизации
- **Средства синхронизации**: мьютекс, RW-блокировка, семафор, атомарные операции, каналы
- **Типовые сбои**: взаимоблокировка, livelock, голодание, инверсия приоритетов
- **Инструменты**: ThreadSanitizer, go test -race, Miri, Helgrind
**Состояние гонки** (англ. *race condition*) — ошибка в программе, при которой результат зависит от непредсказуемого порядка или времени выполнения параллельных действий: потоков, процессов, запросов, корутин. Такие ошибки возникают в [конкурентных](https://llmdocs.ru/wiki/concurrency-parallelism.md) программах, плохо воспроизводятся («у меня работает») и могут проявиться раз в миллион запусков. Основное средство борьбы — **синхронизация**: блокировки, атомарные операции, передача сообщений.[1]

## Классический пример

Два потока увеличивают общий счётчик:

```python
counter = 0

def work():
    global counter
    for _ in range(1_000_000):
        counter += 1      # прочитать → прибавить → записать
```

Операция `counter += 1` неатомарна: это чтение, сложение и запись. Если оба потока прочитали 41, оба запишут 42 — одно увеличение потеряно, и итог меньше 2 000 000. Даже [GIL](https://llmdocs.ru/wiki/gil-free-threading.md) в CPython не спасает: поток может быть прерван между байткод-инструкциями, а в сборках без GIL гонки проявляются ещё чаще.

## Гонка и гонка данных

Различают два понятия:[4]

- **Гонка данных** (*data race*) — два потока одновременно обращаются к одной ячейке памяти, хотя бы один пишет, и между обращениями нет синхронизации. В C, C++ и Go это неопределённое или некорректное поведение по модели памяти языка.[3] Безопасный Rust исключает гонки данных на этапе компиляции через систему [владения и заимствования](https://llmdocs.ru/wiki/ownership-borrowing.md).
- **Логическая гонка** (*race condition*) — более общее понятие: ошибка в порядке действий даже без «сырой» гонки за память. Пример — **TOCTOU** (*time of check to time of use*): программа проверила, что файла нет, а затем создала его, но между проверкой и созданием файл успел создать кто-то другой. Ту же природу имеют двойное списание денег при параллельных запросах и «потерянное обновление» в базе данных.

## Средства синхронизации

**Мьютекс** (*mutual exclusion*) — блокировка, которую в каждый момент держит не больше одного потока. Код между захватом и освобождением называется **критической секцией**.

```python
import threading
lock = threading.Lock()

def work():
    global counter
    for _ in range(1_000_000):
        with lock:            # захват и гарантированное освобождение
            counter += 1
```

```go
var mu sync.Mutex
mu.Lock()
counter++
mu.Unlock()
```

Другие примитивы:

- **RW-блокировка** (`RWMutex`, `RwLock`) — много читателей или один писатель.
- **Семафор** — ограничивает число одновременных владельцев, например не больше 10 соединений к API.
- **Условные переменные** и события — ожидание, пока другой поток не выполнит условие.
- **Атомарные операции** (`sync/atomic`, `std::sync::atomic`, compare-and-swap) — неделимые операции процессора над одним значением, без блокировки; на них строятся lock-free структуры.
- **Передача сообщений** — вместо общей памяти потоки обмениваются данными через очереди или [каналы](https://llmdocs.ru/wiki/goroutines-channels.md): «не общайтесь через разделяемую память, разделяйте память через общение» (поговорка Go).

## Взаимоблокировки и другие сбои

Блокировки решают гонки, но создают свои проблемы:

- **Взаимоблокировка** (*deadlock*) — поток A держит блокировку X и ждёт Y, поток B держит Y и ждёт X; оба стоят вечно. Коффман и соавторы (1971) назвали четыре необходимых условия: взаимное исключение, удержание с ожиданием, отсутствие вытеснения и циклическое ожидание.[5] Самая практичная профилактика — разрушить последнее условие, **всегда захватывая блокировки в одном и том же порядке**; помогают также тайм-ауты (`lock.acquire(timeout=1)`) и `try_lock`.
- **Livelock** — потоки активно уступают друг другу и не продвигаются.
- **Голодание** — поток никогда не получает ресурс, потому что его опережают другие.
- **Инверсия приоритетов** — высокоприоритетная задача ждёт низкоприоритетную, держащую блокировку (знаменитый сбой марсохода Mars Pathfinder в 1997 году).
- **Конкуренция за блокировку** (*contention*) — если критическая секция велика, потоки выстраиваются в очередь и параллелизм пропадает. Секции должны быть короткими: не делать под блокировкой ввод-вывод и сетевые запросы.

## Гонки за пределами потоков

- **Асинхронный код**: в `asyncio` нет вытеснения посреди строки, но между двумя `await` другая корутина может изменить общие данные — для этого есть `asyncio.Lock`.
- **Базы данных**: параллельные транзакции порождают аномалии, от которых защищают [уровни изоляции](https://llmdocs.ru/wiki/isolation-levels.md), `SELECT … FOR UPDATE` и оптимистические блокировки с номером версии (см. [Транзакция и ACID](https://llmdocs.ru/wiki/transaction-acid.md)).
- **Распределённые системы**: два экземпляра сервиса обрабатывают одну задачу; нужны идемпотентность, распределённые блокировки с аренду (lease) или очереди с гарантией доставки.
- **Файловая система**: атомарное создание (`O_CREAT | O_EXCL`), запись во временный файл и `rename`.

## Как находить

- **Детекторы гонок данных**: ThreadSanitizer в Clang и GCC, `go test -race` и `go run -race`,[2][6] Miri для unsafe-кода в Rust, Helgrind в Valgrind. Они находят гонки, которые реально произошли во время прогона, поэтому нужны тесты с параллельной нагрузкой.
- Стресс-тесты, многократный повтор теста, искусственные задержки (`sleep`) в подозрительных местах.
- Проектирование без общего изменяемого состояния: неизменяемые данные, владение одним потоком, очереди задач.

## См. также

- [Конкурентность и параллелизм](https://llmdocs.ru/wiki/concurrency-parallelism.md)
- [asyncio](https://llmdocs.ru/wiki/asyncio.md)
- [MVCC](https://llmdocs.ru/wiki/mvcc.md)
- [Распределённые системы](https://llmdocs.ru/wiki/distributed-systems.md)

## Источники

1. [Herlihy, Shavit — The Art of Multiprocessor Programming (2nd ed.)](https://www.elsevier.com/books/the-art-of-multiprocessor-programming/herlihy/978-0-12-415950-1)
2. [Go — Data Race Detector](https://go.dev/doc/articles/race_detector)
3. [The Go Memory Model](https://go.dev/ref/mem)
4. [The Rustonomicon — Data Races and Race Conditions](https://doc.rust-lang.org/nomicon/races.html)
5. [Coffman, Elphick, Shoshani — System Deadlocks (ACM Computing Surveys, 1971)](https://doi.org/10.1145/356586.356588)
6. [Clang — ThreadSanitizer](https://clang.llvm.org/docs/ThreadSanitizer.html)
7. [Python — threading: Lock objects](https://docs.python.org/3/library/threading.html#lock-objects)
