Являются ли «гонки данных» и «состояние гонки» одним и тем же в контексте параллельного программирования?

Ответы:

142

Нет, это не одно и то же. Они не являются частью друг друга. Они также не являются ни необходимым, ни достаточным условием друг для друга.

Определение гонки данных довольно четкое, и поэтому ее обнаружение можно автоматизировать. Гонка данных возникает, когда 2 инструкции из разных потоков обращаются к одному и тому же участку памяти, по крайней мере, один из этих обращений является записью, и нет синхронизации, которая предписывала бы какой-либо конкретный порядок между этими доступами.

Состояние гонки - это семантическая ошибка. Это ошибка, возникающая во времени или порядке событий, которая приводит к ошибочному поведению программы. Многие состояния гонки могут быть вызваны гонкой данных, но это не обязательно.

Рассмотрим следующий простой пример, где x - общая переменная:

Thread 1    Thread 2

 lock(l)     lock(l)
 x=1         x=2
 unlock(l)   unlock(l)

В этом примере записи в x из потоков 1 и 2 защищены блокировками, поэтому они всегда происходят в некотором порядке, обеспечиваемом порядком, в котором блокировки устанавливаются во время выполнения. То есть атомарность записи не может быть нарушена; всегда существует связь между двумя операциями записи при любом выполнении. Мы просто не можем знать априори, какая запись происходит раньше другой.

Между записями нет фиксированного порядка, потому что блокировки не могут этого обеспечить. Если корректность программ нарушена, например, когда за записью в x потоком 2 следует запись в x в потоке 1, мы говорим, что существует состояние гонки, хотя технически гонки данных нет.

Гораздо полезнее обнаруживать состояния гонки, чем гонки данных; однако этого также очень трудно добиться.

Построение обратного примера также тривиально. Это сообщение в блоге также очень хорошо объясняет разницу на простом примере банковской транзакции.

Барис Касикчи
источник
1
«гонка данных (...) нет синхронизации, которая требует определенного порядка между этими доступами». Я немного смущен. В вашем примере операции могут выполняться в обоих порядках (либо = 1, затем = 2, либо наоборот). Почему это не гонка за данными?
josinalvo
6
@josinalvo: это артефакт технического определения гонки данных. Ключевым моментом является то, что между двумя доступами будет снятие блокировки и получение блокировки (для любого из возможных заказов). По определению, снятие блокировки и получение блокировки устанавливают порядок между двумя доступами, и, следовательно, нет гонки за данными.
Барис Касикчи,
Синхронизация никогда не требует определенного порядка между операциями, так что это не очень удачный способ выразить его. С другой стороны, JMM указывает, что для каждой операции чтения должна быть определенная операция записи, которую он наблюдает, даже в гонке данных. Трудно избежать явного упоминания « происходит до» и порядка синхронизации, но даже определение JLS неверно, говоря о том, что «только что произошло»: по его определению две одновременные изменяемые записи составляют гонку данных.
Марко Топольник
@BarisKasikci "устанавливает порядок", насколько я понимаю, не имеет никакого реального значения. Это просто ласковые слова. Честно говоря, я не верю, что «гонка данных» является отдаленно полезной концепцией, поскольку буквально каждая ячейка памяти, к которой обращаются несколько потоков, может считаться находящейся в «гонке данных»
Нолдорин
Пары «выпуск-получение» всегда устанавливают порядок. Общее объяснение длинное, но тривиальным примером является пара сигнал-ожидание. @Noldorin «Устанавливает порядок» относится к порядку «произошло до», которое является ключевым понятием теории параллелизма (см. Основополагающую статью Лампорта о взаимосвязи «произошло до») и распределенных системах. Гонки данных - полезная концепция, поскольку их присутствие создает множество проблем (например, неопределенная семантика согласно модели памяти C ++, очень сложная семантика в Java и т. Д.). Их обнаружение и устранение составляют обширную литературу в области исследований и практики.
Baris Kasikci
20

Согласно Википедии, термин «состояние гонки» используется со времен появления первых электронных логических вентилей. В контексте Java состояние гонки может относиться к любому ресурсу, например к файлу, сетевому соединению, потоку из пула потоков и т. Д.

Термин «гонка данных» лучше всего зарезервировать для его конкретного значения, определенного JLS .

Самый интересный случай - это состояние гонки, которое очень похоже на гонку данных, но все же не так, как в этом простом примере:

class Race {
  static volatile int i;
  static int uniqueInt() { return i++; }
}

Поскольку iявляется изменчивым, гонка данных отсутствует; однако с точки зрения корректности программы существует состояние гонки из-за неатомарности двух операций: чтения iи записи i+1. Несколько потоков могут получать одно и то же значение из uniqueInt.

Марко Топольник
источник
1
Можете ли вы написать в своем ответе строчку, описывающую, что на data raceсамом деле означает в JLS?
Geek
@geek Слово «JLS» - это гиперссылка на соответствующий раздел JLS.
Марко Топольник
@MarkoTopolnik Меня немного смущает этот пример. Не могли бы вы объяснить: «Поскольку i изменчив, гонка данных отсутствует»? Voltility только гарантирует, что он виден, но все же: 1) он не синхронизирован, и несколько потоков могут читать / писать одновременно и 2) это общее неокончательное поле Итак, согласно Java Concurrency in Practice (также цитируется ниже) , это гонка данных, а не состояние гонки, не так ли?
aniliitb10
@ aniliitb10 Вместо того, чтобы полагаться на цитаты из вторых рук, вырванные из их контекста, вам следует просмотреть раздел JLS 17.4, на который я ссылался в своем ответе. Доступ к изменчивой переменной - это действие синхронизации, как определено в §17.4.2.
Марко Топольник
@ aniliitb10 Голосование не вызывает гонки за данные, потому что доступ к ним можно заказать. То есть вы можете так или иначе аргументировать их порядок, что приведет к разному результату. С гонкой за данные у вас нет возможности обосновать заказ. Например, операция i ++ каждого потока может происходить только с соответствующим им локально кэшированным значением i. В глобальном масштабе у вас нет возможности упорядочить эти операции (с точки зрения программиста) - если у вас нет определенной языковой модели памяти.
Сяо-Фэн Ли,
3

Нет, они разные, и ни один из них не является подмножеством одного или наоборот.

Термин «состояние гонки» часто путают со связанным термином «гонка данных», который возникает, когда синхронизация не используется для координации всего доступа к общему нефинальному полю. Вы рискуете получить гонку данных всякий раз, когда поток записывает переменную, которая может быть прочитана другим потоком, или считывает переменную, которая могла быть последней записана другим потоком, если оба потока не используют синхронизацию; код с гонками данных не имеет полезной определенной семантики в рамках модели памяти Java. Не все состояния гонки являются гонками данных, и не все гонки данных являются состояниями гонки, но оба они могут вызвать непредсказуемый сбой параллельных программ.

Взято из превосходной книги - Java Concurrency in Practice от Джошуа Блоха и Ко.

Ширгилл Фархан
источник
Обратите внимание, что у вопроса есть тег, не зависящий от языка.
martinkunev
1

TL; DR: различие между гонкой данных и состоянием гонки зависит от характера формулировки проблемы и от того, где провести границу между неопределенным поведением и четко определенным, но неопределенным поведением. Текущее различие условно и лучше всего отражает интерфейс между архитектором процессора и языком программирования.

1. Семантика

Гонка данных, в частности, относится к несинхронизированным конфликтующим «доступам к памяти» (или действиям, или операциям) к одной и той же области памяти. Если в доступе к памяти нет конфликта, но все еще существует неопределенное поведение, вызванное упорядочением операций, это состояние гонки.

Обратите внимание, что «доступ к памяти» здесь имеет особое значение. Они относятся к «чистой» загрузке памяти или действиям сохранения без какой-либо дополнительной семантики. Например, хранилище памяти из одного потока не обязательно (обязательно) знает, сколько времени требуется, чтобы данные были записаны в память и, наконец, распространяются в другой поток. В качестве другого примера, сохранение в памяти одного места перед другим сохранением в другом месте одним и тем же потоком не (обязательно) гарантирует, что первые данные, записанные в память, будут впереди вторых. В результате порядок этих обращений к чистой памяти не (обязательно) может быть «обоснован» , и все может произойти, если иное не определено четко.

Когда «доступы к памяти» четко определены с точки зрения упорядочения посредством синхронизации, дополнительная семантика может гарантировать, что даже если время доступа к памяти не определено, их порядок можно «обосновать» посредством синхронизации. Обратите внимание, хотя порядок между обращениями к памяти можно обосновать, они не обязательно являются определяющими, отсюда и состояние гонки.

2. Почему разница?

Но если порядок все еще не определен в состоянии гонки, зачем вообще отличать его от гонки данных? Причина скорее практическая, чем теоретическая. Это потому, что существует различие в интерфейсе между языком программирования и архитектурой процессора.

Команда загрузки / сохранения памяти в современной архитектуре обычно реализуется как «чистый» доступ к памяти из-за природы неупорядоченного конвейера, предположений, многоуровневого кеширования, взаимодействия ЦП и ОЗУ, особенно многоядерных и т. Д. Существует множество факторов, которые приводят к неопределенности в выборе времени и порядка. Принуждение к упорядочиванию каждой инструкции памяти влечет за собой огромные штрафы, особенно в конструкции процессора, поддерживающей многоядерность. Таким образом, семантика упорядочивания снабжена дополнительными инструкциями, такими как различные барьеры (или ограждения).

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

Программисты должны иметь способ обосновать выполнение своего кода. Гонка за данными - это то, что они не могут иметь смысла, поэтому всегда следует избегать (обычно). Вот почему спецификации языка на достаточно низком уровне обычно определяют гонку данных как неопределенное поведение, отличное от четко определенного поведения памяти в состоянии гонки.

3. Модели языковой памяти

Различные процессоры могут иметь разное поведение доступа к памяти, т. Е. Модель памяти процессора. Программистам неудобно изучать модель памяти каждого современного процессора, а затем разрабатывать программы, которые могут извлечь из них пользу. Желательно, чтобы язык мог определять модель памяти, чтобы программы на этом языке всегда вели себя так, как ожидалось, как это определяет модель памяти. Вот почему в Java и C ++ определены свои модели памяти. Разработчики компилятора / среды выполнения обязаны обеспечить применение моделей языковой памяти для разных архитектур процессоров.

Тем не менее, если язык не хочет раскрывать низкоуровневое поведение процессора (и готов пожертвовать некоторыми преимуществами производительности современных архитектур), он может выбрать модель памяти, которая полностью скрывает детали «чистого» обращается к памяти, но применяет семантику упорядочения для всех своих операций с памятью. Затем разработчики компилятора / среды выполнения могут выбрать обработку каждой переменной памяти как изменчивой во всех архитектурах процессора. Для этих языков (которые поддерживают совместную память между потоками) нет гонок данных, но они все равно могут быть условиями гонки, даже с языком полной последовательной согласованности.

С другой стороны, модель памяти процессора может быть более строгой (или менее расслабленной, или на более высоком уровне), например, реализовывать последовательную согласованность, как это делал первые процессоры. Затем все операции с памятью упорядочиваются, и никакая гонка данных не существует ни для каких языков, работающих в процессоре.

4. Вывод

Вернемся к исходному вопросу, ИМХО, это нормально определять гонку данных как частный случай состояния гонки, а состояние гонки на одном уровне может стать гонкой данных на более высоком уровне. Это зависит от характера постановки проблемы и от того, где провести границу между неопределенным поведением и четко определенным, но неопределенным поведением. Просто текущее соглашение определяет границу в интерфейсе язык-процессор, не обязательно означает, что это всегда и должно быть так; но текущее соглашение, вероятно, лучше всего отражает современный интерфейс (и мудрость) между архитектором процессора и языком программирования.

Сяо-Фэн Ли
источник