Гарантии благоприятных условий гонки в C ++

Я знаю, что стандарт C ++ ничего не гарантирует при наличии гонки данных (я считаю, что гонка данных имеет неопределенное поведение, что означает, что все идет, включая завершение программы, изменение случайной памяти и т. Д.).

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

[отредактировано, чтобы заменить "состояние гонки" на "гонку данных"]


person anonymous    schedule 30.03.2014    source источник
comment
Если вы используете файловую систему или файл с отображением в память, то чтение и запись в одном процессе с использованием двух файловых дескрипторов вместо одного, по-видимому, снижает состояние гонки.   -  person Ross Bush    schedule 30.03.2014
comment
Почему тебя это вообще волнует? Такая программа была бы ужасной, потому что вы никогда не сможете безопасно перенести ее на другую архитектуру. Только не надо. Используйте std::mutex или атомикс. Это действительно просто и безопасно.   -  person stefan    schedule 30.03.2014
comment
Причина, по которой меня волнует, заключается в том, что я планирую отобразить область памяти в файле. Существует структура данных, которая указывает, на какую часть файла ссылаются, а на какую - нет. У меня есть читатели в одном потоке, читающие упомянутые области файла, а писатели в другом, пишущие в несвязанную область файла. Я хочу понять, что произойдет, если файл поврежден и писатель напишет в области, которую читает читатель (мне нужен минимум гарантий относительно того, что может произойти, то есть без сбоев, без проблем с безопасностью, ...)   -  person anonymous    schedule 30.03.2014


Ответы (3)


Проблема с гонкой данных не в том, что вы можете прочитать неверное значение на уровне машины. Проблема с гонкой данных заключается в том, что и компилятор, и процессор выполняют множество оптимизаций кода. Чтобы убедиться, что эти оптимизации верны при наличии нескольких потоков, им нужна дополнительная информация о переменных, которые могут совместно использоваться потоками. Такие оптимизации могут, например:

  • переупорядочить операции
  • добавить дополнительные операции загрузки и хранения
  • удалить операции загрузки и сохранения

Есть хорошая статья Ханса Боэма о доброкачественных гонках данных под названием Как неправильно компилировать программы с "доброкачественной" гонкой данных. Следующий отрывок взят из этой статьи:

Двойная проверка на отложенную инициализацию

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

if (!init_flag) {
    lock();
    if (!init_flag) {
        my_data = ...;
        init_flag = true;
    }
    unlock();
}
tmp = my_data;

Ничто не мешает оптимизирующему компилятору переупорядочить настройку my_data на настройку init_flag или даже увеличить нагрузку my_data до первого теста init_flag, перезагружая ее в условном выражении, если init_flag не был установлен. Некоторое оборудование, отличное от x86, может выполнять аналогичные переупорядочения, даже если компилятор не выполняет преобразование. Любой из них может привести к тому, что при окончательном чтении my_data будет обнаружено неинициализированное значение и будут получены неверные результаты.


Вот еще один пример, где int x - общая, а int r - локальная переменная.

int r = x;
if (r == 0)
    printf("foo\n");
if (r != 0)
    printf("bar\n");

Если бы мы только сказали, что чтение x приводит к неопределенному значению, тогда программа напечатала бы либо «foo», либо «bar». Но если компилятор преобразует код следующим образом, программа может также распечатать обе строки или ни одну из них.

if (x == 0)
    printf("foo\n");
if (x != 0)
    printf("bar\n");
person nosid    schedule 30.03.2014
comment
В примере с двойной проверкой компилятор может изменить порядок вещей. Разве это не было бы эквивалентно предположению, что init_flag всегда возвращает true после гонки данных? Разве нельзя сказать, что программа ведет себя так, как будто операции чтения достаточно читают неопределенное значение? - person anonymous; 30.03.2014

вы можете использовать ОС Linux, в которой вы можете разветвить 2 или более дочерних процесса над родительским процессом в С ++, вы можете сделать оба для доступа к одной области памяти и, используя синхронизацию, вы можете достичь того, что хотите сделать -> Как разделить память между процессами fork ()?, http://en.wikipedia.org/wiki/Dekker 's_algorithm, http://en.wikipedia.org/wiki/Readers%E2%80%93writers_problem,

person Saurabh Saluja    schedule 30.03.2014

Один пример, который всегда будет приводить к участию в гонке: попросите два потока записать разные значения в одну и ту же переменную. Предположим, что

  • поток один устанавливает переменную a в 1
  • thread two устанавливает переменную a в 2

Вы получите состояние гонки даже с мьютексом, например, потому что

  • если сначала выполняется поток 1, то вы получаете a = 1, затем a = 2.
  • если сначала выполняется поток номер два, то вы получаете a = 2, затем a = 1.

Порядок потоков зависит от ОС, и нет гарантии, какой поток будет первым. В противном случае это было бы последовательным, и не нужно было бы делать это в отдельных потоках.

Теперь предположим, что у вас вообще нет синхронизации и вы выполняете a = a + 1 в первом потоке a = a + 2 во втором потоке. Начальное значение a равно 0.

При сборке генерируемый код копирует значение a в один регистр, прибавляет к нему 1 (в случае первого потока, в противном случае - 2).

Если у вас нет синхронизации вообще, вы можете иметь следующий порядок, например

  • Thread1: значение a скопировано в reg1. reg1 содержит 0

  • Thread2: значение a скопировано в reg2. reg2 содержит 0

  • Thread1: добавлено значение reg1 1. Теперь содержит 1

  • Thread2: добавлено значение reg2 2. Теперь содержит 2

  • Thread1: добавлено значение reg1 1. Теперь содержит 1

  • Thread2: добавлено значение reg2 2. Теперь содержит 2

  • Thread1: значение reg1 помещается в a. Теперь a содержит 1

  • Thread2: значение reg2 помещается в a. Теперь a содержит 2

Если у вас выполняется поток 1, затем последовательно поток 2, в конце будет = 3.

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

Имеет смысл?

person Gabriel    schedule 30.03.2014
comment
Да, в этом есть смысл. Мой вопрос в том, может ли гонка данных привести к чему-то другому, кроме получения странного значения (может ли это привести к завершению программы, обновлению других случайных переменных и т. Д. В истинном неопределенном поведении) - person anonymous; 30.03.2014
comment
У вас будет неопределенное поведение, даже с мьютексом нет, не будет. UB имеет очень специфическое значение в C ++, а именно: может произойти ВСЕ. Если вы используете мьютексы или атомики для защиты доступа к a, тогда вы можете иметь состояние гонки в том смысле, что результат зависит от скорости выполнения (какой бы поток ни выполнялся последним, выиграет), но вы не будет гонки данных и, следовательно, не будет неопределенного поведения - вы получите одно из двух значений. - person MikeMB; 21.04.2015