Гонка данных
- title
- Гонка данных
- type
- concept
- summary
- Неупорядоченный параллельный доступ к одной ячейке памяти с хотя бы одной записью; отличие от общей гонки и методы поиска через lockset и векторные часы
- tags
- concurrency, debugging, memory-models
- created
- 2026-09-13
- updated
- 2026-09-13
- lang
- ru
- translation_of
- data-race
- source_updated
- 2026-09-13
- translated
- 2026-09-14
- translator
- lllm/antigravity/gemini-3.7-flash-medium
Гонка данных (data race) - это два обращения к одной и той же ячейке памяти из разных потоков, как минимум одно из которых является записью, при отсутствии механизмов, задающих порядок между ними. Стандарт C11 формулирует это как два конфликтующих действия в разных потоках, где хотя бы одно не атомарно и ни одно из них не предшествует другому (happens before), объявляя результат неопределённым поведением (undefined behaviour). Модель памяти Golang определяет её как запись, происходящую одновременно с другим чтением или записью той же ячейки, если только все обращения не выполняются через sync/atomic.
Это определение говорит о порядке выполнения, а не об ошибках. Программа с гонкой данных может проходить все тесты (как пример со счётчиком в threadsanitizer-limits вплоть до N=10000), но скрытый баг в ней остаётся независимо от того, заметил его тест или нет.
Гонка данных в сравнении с общей гонкой
Обычное состояние гонки (race condition, общая гонка) - это ошибка корректности, зависящая от порядка планирования потоков. Эти понятия пересекаются, но не тождественны. Классическая иллюстрация - операция чтения-модификации-записи со счётчиком: v = counter; counter = v + 1. Без блокировки это одновременно и гонка данных, и общее состояние гонки. Если разнести чтение и запись по двум отдельным критическим секциям, гонка данных исчезнет, поскольку каждое обращение теперь упорядочено мьютексом, однако инкременты всё равно будут теряться. Ни один детектор гонок данных не сообщит о такой версии: с его точки зрения сообщать не о чем.
Именно из-за этого различия инструменты, созданные для одного класса проблем, оставляют незамеченной большую часть ошибок многопоточности. В исследовании реальных багов в Golang, проведённом Tu et al (см. message-passing-shared-mutable-state), детектор гонок поймал примерно половину неблокирующих ошибок. Каналы устраняют гонки данных, но не устраняют ошибки координации - именно этот тезис приводится в message-passing-vs-shared-memory и классификации из go-channel-bug-patterns.
Как детекторы находят гонки
Гонки данных в принципе можно находить автоматически, без каких-либо аннотаций со стороны программиста. Динамические детекторы инструментируют каждое обращение к памяти и примитивы синхронизации, а затем проверяют каждое наблюдаемое исполнение.
Lockset-детекторы, начиная с Eraser (1997), фиксируют, какие блокировки удерживаются при каждом обращении к разделяемой ячейке, и предупреждают, когда пересечение этих множеств оказывается пустым. Они выявляют гонки, которые случайным образом не проявились в конкретном чередовании потоков, но не понимают паттерны упорядочивания вроде ожидания завершения потока (thread join), что приводит к ложным срабатываниям, а также не видят гонок в коде, который вообще не использует блокировки.
Happens-before детекторы вычисляют отношение частичного порядка напрямую. FastTrack выделяет каждому потоку логические часы и векторные часы его знаний о других потоках, объединяет векторы при захвате блокировки (acquire), публикует их при освобождении (release) и сигнализирует об обращении, если последнее конфликтующее обращение не покрывается вектором текущего потока. ThreadSanitizer v2 и v3 работают по схожему принципу; именно их используют Clang, GCC, Golang, Swift и OCaml.
Happens-before детектор знает только те порядки, которые реально возникли в ходе запуска, и в этом его главная слабость. Посторонний мьютекс, захваченный в удачном порядке, создаёт ребро порядка между двумя несинхронизированными записями. Кроме того, TSan ограничивает собственную память фиксированными лимитами (255 слотов под потоки, 14-битные часы, четыре записи об обращениях на 8-байтовую гранулу), а sync.Pool в Golang намеренно скармливает детектору синтетические порядки. В threadsanitizer-limits показано, как каждый из этих факторов маскирует очевидную гонку в C и Golang.
Почему модели языков их запрещают
В shared-memory-consistency-causality отсутствие гонок данных - это условие, делающее возможной простую модель памяти: последовательная согласованность (sequential consistency) для атомарных операций сохраняется при условии отсутствия гонок данных в неатомарных обращениях (SC+DRF), на что опираются Java и Golang. Несинхронизированный доступ становится побочным каналом, через который можно наблюдать реальный, более слабый аппаратный порядок исполнения. Именно поэтому стандарты C и C++ классифицируют гонки данных как undefined behaviour, вместо того чтобы закреплять за ними определённый результат: фиксированный результат потребовал бы от каждой архитектуры скрывать отсутствие write-atomicity.
Связанные страницы
- threadsanitizer-limits - работающий детектор в духе FastTrack и лимиты памяти TSan, из-за которых пропускаются гонки
- shared-memory-consistency-causality - что означает "happens before" на уровне когерентности кэшей
- message-passing-vs-shared-memory - что устраняет и чего не устраняет отказ от разделяемой памяти
- gosentry-go-fuzzing-fork - фаззинг со включённым детектором гонок