Что гарантировано с C ++ std :: atomi c на уровне программиста? - PullRequest
9 голосов
/ 31 января 2020

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

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

[T1: thread1 T2: thread2 V1: shared atomi c переменная]

I понимать, что std :: atomi c гарантирует, что

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

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

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

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

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

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

(4) std::atomic НЕ ГУ Гарантируйте, что T2 не будет читать 'устаревшее' значение на атоме c read (V) после атоми c write (V) на T1.

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

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

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

Обновление 1

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

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

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

Обновление 2

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

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

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

  • T1
  • T1

это верно?

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

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

Ответы [ 2 ]

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

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

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

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

Обновление, например,

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

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

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

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

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

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

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

Обновление 2

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

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

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

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

1 голос
/ 31 января 2020

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

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

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

...