Что формально гарантирует, что чтение не видит запись (с условием гонки) в других потоках? - PullRequest
1 голос
/ 19 июня 2019

Это вопрос о формальных гарантиях стандарта C ++.

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

// Global state
int x = 0, y = 0;

// Thread 1:
r1 = x;
if (r1 == 42) y = r1;

// Thread 2:
r2 = y;
if (r2 == 42) x = 42;

Стандарт прямо говорит, что такое поведение разрешено спецификацией для атомарных объектов:

[Примечание: требования допускают r1 == r2 == 42 в следующих Например, x и y изначально равны нулю:

// Thread 1:
r1 = x.load(memory_order_relaxed);
if (r1 == 42) y.store(r1, memory_order_relaxed);
// Thread 2:
r2 = y.load(memory_order_relaxed);
if (r2 == 42) x.store(42, memory_order_relaxed);

Однако реализации не должны допускать такого поведения. - конец примечания]

Какая часть так называемой "модели памяти" защищает неатомарные объекты от этих взаимодействий, вызванных чтениями, которые видят взаимодействие ?

Ответы [ 3 ]

10 голосов
/ 19 июня 2019

Когда возможно состояние гонки, что гарантирует, что чтение общей переменной (нормальное, не атомарное) не может увидеть запись

Нет такой гарантии.

При наличии условия гонки поведение программы не определено:

[intro.races]

Два действия потенциально одновременны, если

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

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

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

Два обращения к одному и тому же объекту типа volatile std::sig_­atomic_­t не приводят к гонке данных, если оба происходят в одном и том же потоке, даже если один или несколько происходит в обработчике сигналов. ...

7 голосов
/ 19 июня 2019

Какая часть так называемой "модели памяти" защищает неатомарные объекты от этих взаимодействий, вызванных чтениями, которые видят взаимодействие?

None. Фактически, вы получаете противоположное, и стандарт явно вызывает это как неопределенное поведение. В [intro.races] \ 21 у нас есть

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

, который охватывает ваш второй пример.


Правило состоит в том, что если у вас есть общие данные в нескольких потоках, и хотя бы один из этих потоков записывает в эти общие данные, сами данные должны быть atomic<> или вам нужна синхронизация, чтобы гарантировать, что записи происходят, когда никакие другие нить читает или пишет. Без этого у вас есть гонка данных и неопределенное поведение. (Несколько одновременных несинхронизированных считывателей хороши, если нет писателей.)

Обратите внимание, что volatile не является допустимым механизмом синхронизации. Вам нужны атомные / мьютексные / условные переменные для защиты общего доступа. (Правильное использование атомик с помощью memory_order_acquire, release или seq_cst может позволить вам создать собственное взаимное исключение, делая его безопасным для чтения или записи неатомарной переменной, например, в узел по кругу - очередь в буфере после выполнения действия, которое гарантирует, что этот поток зарезервировал этот узел для себя. Вот почему ISO C ++ 11 определяет порядок памяти для атомарных элементов в терминах создания отношений «синхронизирует с» между писателем и читателем.)

Одновременное несинхронизированное чтение / запись переменной std::atomic<> не является UB, но может быть ошибкой «состояния гонки» в вашей логике, если ваш код работает корректно только для некоторых возможных порядков чтения и записи.

4 голосов
/ 19 июня 2019

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

В практическом смысле, здесь приведен порядок событий, которые могут произойти в потоке 1, которыйбыло бы вполне разумно, но в результате поведение, которое вы считаете запрещенным, даже если аппаратное обеспечение гарантировало, что весь доступ к памяти был полностью сериализован между процессорами.Имейте в виду, что стандарт должен учитывать не только поведение оборудования, но и поведение оптимизаторов, которые часто агрессивно переупорядочивают и переписывают код.

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

old_y = y; // old_y is a hidden variable (perhaps a register) created by the optimizer
y = 42;
if (x != 42) y = old_y;

У оптимизатора могут быть совершенно разумные причины для этого.Например, он может решить, что гораздо вероятнее, чем не записать 42 в y, и по причинам зависимости конвейер может работать намного лучше, если сохранение в y происходит раньше, а не позже.

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

Атомарные переменные накладывают ограничения на способность компилятора переписывать код, а такжепоручение компилятору выдавать специальные инструкции ЦП, которые накладывают ограничения на способность ЦП переупорядочивать доступ к памяти.Ограничения, включающие memory_order_relaxed, намного сильнее, чем обычно.Компилятору обычно разрешают полностью избавиться от любых ссылок на x и y, если они не являются атомарными.

Кроме того, если они атомарные, компилятор должен гарантировать, что другие процессорыувидеть всю переменную как с новым или старым значением.Например, если переменная является 32-разрядным объектом, который пересекает границу строки кэша, а модификация включает в себя изменение битов по обе стороны от границы строки кэша, один ЦП может увидеть значение переменной, которое никогда не записывается, потому что он видит толькообновление битов на одной стороне границы строки кэша.Но это недопустимо для атомарных переменных, измененных с помощью memory_order_relaxed.

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

...