EnglishРусский Map

Гонка данных

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 - фаззинг со включённым детектором гонок