Почему «приобретение/выпуск» не может гарантировать последовательную согласованность в С++ 11?

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

-Thread 2-
if (x.load(memory_order_acquire) == 10) {
  assert (y.load(memory_order_acquire) == 20);
  y.store (10, memory_order_release)
}

-Thread 3-
if (y.load(memory_order_acquire) == 10) {
  assert (x.load(memory_order_acquire) == 10);
}

В параграфе GCC Atomic Wiki "Общее резюме" говорится, что приведенный выше код assert(x.load(memory_order_acquire)) может не удалось. Но я не понимаю, почему?

Мое понимание:

  1. Thread3 не может изменить порядок LoadLoad из-за барьера acquire.
  2. Thread1 не может изменить порядок StoreStore из-за барьера выпуска.
  3. Когда Thread2 читает (x) -> 10, x должен быть сброшен из storebuffer в кеш в Thread1, чтобы каждый поток знал, что значение x изменилось, например, сделать строку кэша недействительной.
  4. Thread3 использует барьер Acquire, поэтому он может видеть x(10).

person Pengcheng    schedule 21.05.2018    source источник
comment
Я не понимаю, почему Если только статья Вики не изменилась за последний час (и хотя я не умею ориентироваться в их обратной системе Вики, я не вижу ее в разделе «Недавние изменения»), то, что вы опубликовали не тот же код, что и в примере. Так вот почему вы запутались. В примере на этой странице всегда используется relaxed; он никогда не использует acquire/release.   -  person Nicol Bolas    schedule 21.05.2018
comment
@NicolBolas Это в разделе «Общее резюме», а затем в подразделе «режим выпуска / приобретения».   -  person LWimsey    schedule 21.05.2018
comment
@LWimsey: Нет, это не так. В общей сводке есть некоторый код, который использует memory_order_relaxed и memory_order_seq_cst в качестве первых двух атомарных операций. И в разделе режима освобождения/приобретения нет примера, соответствующего этому коду. В нем есть что-то похожее на часть этого кода, но не на весь код. И в разделе конкретно говорится, что утверждение не сработает. Просто найдите y.load(memory_order_acquire) == 10; вы не найдете его на этой странице.   -  person Nicol Bolas    schedule 21.05.2018
comment
Подраздел освобождения/получения подразумевает, что все сохранения являются операциями освобождения, а все загрузки — операциями получения.   -  person LWimsey    schedule 21.05.2018
comment
@LWimsey: ... Я не уверен, что понимаю, как это относится к тому, что я говорю. ОП опубликовал код, а затем утверждает, что вики GCC говорит, что этот код не работает. Вики GCC ничего подобного не говорит. Он не предоставляет этот код. Это не говорит о том, что данный код не работает. Вопрос некорректен.   -  person Nicol Bolas    schedule 21.05.2018
comment
@NicolBolas Опять же, если вы читаете раздел «Общее резюме», в первой строке буквально говорится: исследовать этот случай для каждой из различных моделей памяти.. А затем они показывают код без параметров заказа. Для случая Acq/Rel это дает именно тот код, который показывает OP.   -  person LWimsey    schedule 21.05.2018
comment
Вики @NicolBolas GCC: режим выпуска/получения требует синхронизации только двух задействованных потоков. Это означает, что синхронизированные значения не коммутативны другим потокам. Утверждение в потоке 2 все еще должно быть истинным, поскольку потоки 1 и 2 синхронизируются с x.load(). Поток 3 не участвует в этой синхронизации, поэтому, когда потоки 2 и 3 синхронизируются с y.load(), утверждение потока 3 может завершиться ошибкой. Синхронизации между потоками 1 и 3 не было, поэтому для 'x' здесь нельзя принять никакого значения.   -  person Pengcheng    schedule 21.05.2018
comment
@Pengcheng синхронизированные значения не коммутативны для других потоков коммутативны?   -  person curiousguy    schedule 01.07.2018


Ответы (2)


Это плохой пример, хотя я полагаю, он иллюстрирует, насколько деформирующими могут быть расслабленные атомы.

[intro.execution]p9:

Каждое вычисление значения и побочный эффект, связанные с полным выражением, упорядочиваются перед каждым вычислением значения и побочным эффектом, связанным со следующим оцениваемым полным выражением.

[атомный порядок]p2:

Атомарная операция A, которая выполняет операцию освобождения атомарного объекта M, синхронизируется с атомарной операцией B, которая выполняет операцию получения M и получает свое значение от любого побочного эффекта в последовательности выпуска, возглавляемой A.

В результате показанные оценки связаны друг с другом последовательностью до и синхронизацией с отношениями:

Thread 1                   Thread 2              Thread 3

y.store(20)
   |
   | s.b.
   V           s.w.
x.store(10)  -------->  x.load() == 10
                               |
                               | s.b.
                               V      s.w.
                        y.store(10) --------> y.load() == 10
                                                  |
                                                  | s.b.
                                                  V
                                              x.load() == ?

Таким образом, каждая оценка в цепочке происходит перед следующей (см. [intro.races]стр. 9–10).

[intro.races] p15,

Если вычисление значения A атомарного объекта M происходит до вычисления значения B для M и A берет свое значение из побочного эффекта X на M, тогда значение, вычисленное B, должно быть сохраненным значением на X или значение, сохраненное побочным эффектом Y в M, где Y следует за X< /em> в порядке изменения M.

Здесь A — это нагрузка в потоке 2, которая приняла значение 10, B — это нагрузка в потоке 3 (в утверждении). Поскольку A происходит раньше, чем B, и нет других побочных эффектов на x, B также должно читаться как 10.


У Херба Саттера есть гораздо более простой пример в его блоге< /а>:

T1: x = 1;
T2: y = 1;
T3: if( x == 1 && y == 0 ) puts("x first");
T4: if( y == 1 && x == 0 ) puts("y first");

Вам абсолютно необходима последовательная согласованность, чтобы гарантировать печать не более одной строки.

person T.C.    schedule 21.05.2018
comment
Я думаю, что люди из gcc Wiki правы. Отношение «происходит до», которое устанавливается между потоками 1 и 2, применяется к операциям, выполняемым до выпуска (в x) в потоке 1, и операциям, выполняемым после получения в потоке 2. Технически , переменная x не является частью этой последовательности до/после, и они могут быть правы в том, что она может стать видимой не по порядку в потоке 3. - person LWimsey; 21.05.2018
comment
@LWimsey Думаю, можно с уверенностью сказать, а? Ответ доказанный x должен читаться как 10 - person Passer By; 21.05.2018
comment
@LWimsey Если они намереваются реализовать модель памяти C ++ 11, то они совершенно ошибаются. - person T.C.; 22.05.2018
comment
@Т.С. Я согласен. Убедительным аргументом является то, что две нагрузки из x упорядочены. - person LWimsey; 22.05.2018
comment
@LWimsey Более сильный аргумент в отношении стандартного стандарта заключается в том, чтобы игнорировать весь стандартный текст (который очень часто является полным мусором) и предполагать, что вместо него было написано что-то другое (пригодное для использования). Если вы не можете предположить, что получение в Ta (наблюдение) выпущенной (опубликованной) истории другим потоком Tb также не помещает в прошлое Ta наблюдения Tb, включая прошлое других потоков Tx, которые Tb поместил в свой прошлое путем получения освобожденного состояния, то получение мьютекса было бы практически бесполезным. - person curiousguy; 08.07.2019

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

Это означает, что любой магазин:

  • это операция освобождения
  • передает значение, указывающее точную точку в ходе одного потока

Форма должна быть именно A.store (C, memory_order_release); где:

  • A является атомарным объектом
  • C является константой

а пара (A,C) однозначно характеризует строку программы.

И наоборот каждая нагрузка:

  • является приобретением
  • проверяет только определенное значение

Форма должна быть именно

if (A.load(memory_order_acquire) == C) { ... }

где нет пункта else.

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

person curiousguy    schedule 23.11.2019