C ++ std :: memory_order_relaxed путаница

Oct 12 2020

Я читал эту статью из GCC Wiki о барьерах памяти C ++ (и это круто).
Пока я не добрался до этого момента, это было довольно просто:

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

-Thread 1-
y.store (20, memory_order_relaxed)
x.store (10, memory_order_relaxed)

-Thread 2-
if (x.load (memory_order_relaxed) == 10)
  {
    assert (y.load(memory_order_relaxed) == 20) /* assert A */
    y.store (10, memory_order_relaxed)
  }

-Thread 3-
if (y.load (memory_order_relaxed) == 10)
  assert (x.load(memory_order_relaxed) == 10) /* assert B */

Поскольку потоки не нужно синхронизировать в системе, любой из assert в этом примере может на самом деле FAIL.

Хорошо, это тоже просто, давайте продолжим ...

-Thread 1-
x.store (1, memory_order_relaxed)
x.store (2, memory_order_relaxed)

-Thread 2-
y = x.load (memory_order_relaxed)
z = x.load (memory_order_relaxed)
assert (y <= z)

Утверждение не может потерпеть неудачу. Как только поток 2 видит хранилище 2, он больше не может видеть значение 1. Это предотвращает объединение ослабленных нагрузок одной переменной с ослабленными нагрузками другой ссылки, которая может быть псевдонимом.

Вот что меня смутило: почему y не может загрузить значение 2, а z загрузить значение 1 (и привести к сбою утверждения), поскольку порядок не синхронизирован в потоке 1?

Ответы

4 NicolBolas Oct 12 2020 at 12:12

Ослабленное упорядочение относится к упорядочиванию операций по отношению к другим доступам к памяти , а не к упорядочиванию относительно релакс-модифицированного атома. В вашем первом случае тот факт, что вы видите 10 в xдюймах, ничего не значит в отношении значения y. И наоборот.

Но ваш второй случай отличается, потому что он влияет на тот же атомарный объект.

[intro.races] / 10 сообщает нам, что в потоке, если одна операция выполняется раньше другой, то эта операция «происходит раньше» другой. И [intro.races] / 14-17 описывают следующее поведение в отношении атомики :

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

И вот что у вас есть. Все изменения происходят с одним и тем же объектом, поэтому они должны происходить в определенном порядке. Даже если этот порядок не может быть определен точно, он должен соответствовать отношениям кода «происходит раньше».

Две операции потока 1 упорядочены отношением «происходит раньше». И операции потока 2 сами упорядочиваются отношением «происходит раньше».

Поскольку все они действуют на один и тот же атомарный объект, если он yполучает значение 2, значит, "произошло после" xбыло установлено значение 2. Таким образом, порядок доступа для xдолжен был быть "x = 1, x = 2, прочтите x ". И поскольку последнее чтение xпроисходит после первого чтения x, полученное значение не может быть 1.