Что гарантировано с C ++ std :: atomic на уровне программиста?

9

Я выслушал и прочитал несколько статей, выступлений и вопросов о стековом потоке std::atomicи хотел бы быть уверен, что хорошо это понял. Потому что я все еще немного запутался с видимостью записи строк кэша из-за возможных задержек в протоколах когерентности кэша MESI (или производных), буферах хранения, недействительных очередях и так далее.

Я читал, что у x86 более сильная модель памяти, и что, если аннулирование кэша задерживается, x86 может восстановить запущенные операции. Но сейчас меня интересует только то, что я должен считать программистом C ++, независимо от платформы.

[T1: поток1 T2: поток2 V1: общая атомарная переменная]

Я понимаю, что std :: atomic гарантирует, что

(1) В переменной не происходит скачек данных (благодаря эксклюзивному доступу к строке кэша).

(2) В зависимости от того, какой memory_order мы используем, он гарантирует (с барьерами), что последовательная согласованность происходит (до барьера, после барьера или обоих).

(3) После атомарной записи (V1) в T1, атомарный RMW (V1) в T2 будет когерентным (его строка кэша будет обновлена ​​с записанным значением в T1).

Но, как упомянуть пример когерентности кеша ,

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

Итак, верно ли следующее?

(4) std::atomicНЕ гарантирует, что T2 не будет считывать «устаревшие» значения при атомарном чтении (V) после атомарной записи (V) в T1.

Вопросы, если (4) верно: если атомарная запись на T1 делает недействительной строку кэша независимо от задержки, почему T2 ожидает, чтобы аннулирование было эффективным, когда выполняется атомарная операция RMW, но не на атомарном чтении?

Вопросы, если (4) неверно: когда поток может прочитать «устаревшее» значение и «это видно» при выполнении, тогда?

Я очень ценю ваши ответы

Обновление 1

Так что, похоже, я ошибся в (3) тогда. Представьте себе следующее чередование для начального V1 = 0:

T1: W(1)
T2:      R(0) M(++) W(1)

Несмотря на то, что RMW T2 гарантированно произойдет полностью после W (1) в этом случае, он все равно может считывать «устаревшее» значение (я был не прав). В соответствии с этим, atomic не гарантирует полную когерентность кэша, только последовательную согласованность.

Обновление 2

(5) Теперь представьте этот пример (x = y = 0 и являются атомарными):

T1: x = 1;
T2: y = 1;
T3: if (x==1 && y==0) print("msg");

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

  • Т1 <Т3 <Т2
  • T1 <T2 <T3 (где T3 видит x = 1, но еще не y = 1)

это правильно?

(6) Если поток всегда может прочитать «устаревшие» значения, что произойдет, если мы выберем типичный сценарий «публикации», но вместо того, чтобы сигнализировать, что некоторые данные готовы, мы делаем наоборот (удаляем данные)?

T1: delete gameObjectPtr; is_enabled.store(false, std::memory_order_release);
T2: while (is_enabled.load(std::memory_order_acquire)) gameObjectPtr->doSomething();

где T2 все еще будет использовать удаленный ptr, пока не увидит, что is_enabled имеет значение false.

(7) Кроме того, тот факт, что потоки могут читать «устаревшие» значения, означает, что мьютекс не может быть реализован только с одним атомарным правом без блокировки? Это потребовало бы механизма синхронизации между потоками. Требуется ли блокируемый атом?

Альберт Калдас
источник

Ответы:

3
  1. Да, нет данных гонок
  2. Да, с соответствующими memory_orderзначениями вы можете гарантировать последовательную согласованность
  3. Элементарное чтение-изменение-запись всегда будет происходить полностью до или полностью после атомарной записи в одну и ту же переменную.
  4. Да, T2 может прочитать устаревшее значение из переменной после атомарной записи в T1

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

Потоки всегда могут прочитать устаревшие значения, за исключением случаев, когда происходит раньше, чем гарантирует относительное упорядочение .

Если операция RMW считывает «устаревшее» значение, то она гарантирует, что сгенерированная запись будет видима до любых записей из других потоков, которые будут перезаписывать прочитанное значение.

Обновить например

Если T1 пишет, x=1а T2 делает x++, с xизначально 0, выбор с точки зрения хранения x:

  1. Сначала записывается T1, поэтому T1 записывает x=1, затем T2 считывает x==1, увеличивает это значение до 2 и записывает обратно x=2как одну атомарную операцию.

  2. Запись Т1 - вторая. T2 читает x==0, увеличивает его до 1 и записывает обратно x=1как одну операцию, затем T1 пишет x=1.

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

Таким образом, T1 может выдать x=1, а затем продолжить с другими вещами, даже если T2 все равно будет читать x==0(и, следовательно, записывать x=1).

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

Это наиболее очевидно, если у вас есть условие на значение, считанное из операции RMW.

Обновление 2

  1. Если вы используете memory_order_seq_cst(по умолчанию) для всех атомарных операций, вам не нужно беспокоиться о подобных вещах. С точки зрения программы, если вы видите «msg», то запускается T1, затем T3, затем T2.

Если вы используете другие порядок памяти (особенно memory_order_relaxed), то вы можете увидеть другие сценарии в вашем коде.

  1. В этом случае у вас есть ошибка. Предположим, что is_enabledфлаг равен true, когда T2 входит в свой whileцикл, поэтому он решает запустить тело. T1 теперь удаляет данные, а T2 затем задерживает указатель, который является висящим указателем, и возникает неопределенное поведение . Атомика никоим образом не помогает и не мешает, кроме предотвращения гонки данных на флаге.

  2. Вы можете реализовать мьютекс с одной атомарной переменной.

Энтони Уильямс
источник
Большое спасибо @Anthony Wiliams за быстрый ответ. Я обновил свой вопрос примером RMW, считывающим устаревшее значение. Глядя на этот пример, что вы подразумеваете под относительным упорядочением и что W (1) в T2 будет виден до любой записи? Означает ли это, что как только T2 увидит изменения T1, он больше не будет читать W (1) T2?
Альберт Калдас
Таким образом, если «потоки всегда могут прочитать устаревшие значения», это означает, что согласованность кэша никогда не гарантируется (по крайней мере, на уровне программиста на c ++). Не могли бы вы взглянуть на мое обновление2, пожалуйста?
Альберт Калдас
Теперь я вижу, что я должен был уделять больше внимания языку и моделям аппаратной памяти, чтобы полностью понять все это, то, чего мне не хватало. большое спасибо!
Альберт Калдас
1

Относительно (3) - это зависит от используемого порядка памяти. Если обе std::memory_order_seq_cstоперации - хранилище и операция RMW , то обе операции упорядочены каким-либо образом, т. Е. Либо сохранение происходит до RMW, либо наоборот. Если в хранилище есть порядок перед RMW, то гарантируется, что операция RMW «увидит» сохраненное значение. Если хранилище упорядочено после RMW, оно перезапишет значение, записанное операцией RMW.

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

Если вы хотите прочитать еще одну статью, я могу отослать вас к Модели памяти для программистов на C / C ++ .

mpoeter
источник
Спасибо за статью, я еще не читал ее. Даже если он довольно старый, было бы полезно собрать мои идеи вместе.
Альберт Калдас
1
Рад слышать это - эта статья является немного расширенной и пересмотренной главой из моей магистерской диссертации. :-) Это фокусируется на модели памяти, представленной C ++ 11; Я мог бы обновить его, чтобы отразить (небольшие) изменения, внесенные в C ++ 14/17. Пожалуйста, дайте мне знать, если у вас есть какие-либо комментарии или предложения по улучшению!
mpoeter