Подтвердить что ты не робот

Разница между memory_order_consume и memory_order_acquire

У меня есть вопрос относительно статьи GCC-Wiki. Под заголовком "Общее резюме" приведен следующий пример кода:

Тема 1:

y.store (20);
x.store (10);

Тема 2:

if (x.load() == 10) {
  assert (y.load() == 20)
  y.store (10)
}

Говорят, что если все магазины релиз, и все нагрузки приобретать, утверждение в потоке 2 не может потерпеть неудачу. Это ясно для меня (поскольку хранилище x в потоке 1 синхронизируется с нагрузкой от x в потоке 2).

Но теперь приходит часть, которую я не понимаю. Также сказано, что если все магазины релиз, а все нагрузки потребляют, результаты будут одинаковыми. Не было бы возможным, чтобы загрузка из y была поднята до нагрузки от x (потому что между этими переменными нет зависимости)? Это означало бы, что утверждение в потоке 2 действительно может потерпеть неудачу.

4b9b3361

Ответ 1

Правило C11 Standard выглядит следующим образом.

5.1.2.4 Многопоточные исполнения и расписания данных

  1. Оценка A является зависимой от заказа до 16) оценкой B, если:

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

    - для некоторой оценки X, A является зависимым от нормы до X и X несет зависимость от B.

  2. Оценка. Межпотоковая передача происходит до оценки B, если A синхронизируется с B, A является упорядоченной зависимостью до B или для некоторой оценки X:

    - A синхронизируется с X и X секвенируется до B,

    - A секвенирован до того, как X и X межпоточность произойдет до B, или

    - Между потоками происходит до того, как X и X межпоточные события произойдут до B.

  3. ПРИМЕЧАНИЕ 7 "Интер-поток" происходит до того, как отношение описывает произвольные конкатенации '', секвенированные ранее, '' синхронизирует с и '' зависимость, упорядоченную до отношений, с двумя исключениями, Первое исключение состоит в том, что конкатенация не разрешается заканчивать "упорядоченной по заказу", а затем "секвентированной ранее". Причина этого ограничения заключается в том, что операция потребления, участвующая в "зависимой от зависимостей до отношения", обеспечивает упорядочение только по отношению к операциям, к которым эта операция потребления фактически несет зависимость. Причина, по которой это ограничение применяется только к конец такой конкатенации состоит в том, что любая последующая операция освобождения обеспечит требуемый порядок для предварительной обработки. Второе исключение состоит в том, что конкатенация не разрешается полностью состоять из "секвенированных ранее". Причины этого ограничения заключаются в следующем: (1) разрешить "межпоточность" до того, как быть транзитивно закрытой, и (2) "происходит до того, как отношение, определенное ниже, предусматривает отношения, состоящие целиком из" секвенированных ранее ".

  4. Оценка A происходит перед оценкой B, если A секвенирован до B или. Межпоточная передача происходит до B.

  5. Видимый побочный эффект A на объекте M относительно вычисления значений B из M удовлетворяет условиям:

    - A происходит до B и

    - нет другого побочного эффекта от X до M, такого, что A происходит до того, как X и X произойдет до B.

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

(выделено курсивом)


В комментарии ниже я сокращу ниже:

  • Зависимость, упорядоченная до: DOB
  • Интер-поток происходит до: ITHB
  • Бывает раньше: HB
  • Последовательность перед: SeqB

Давайте рассмотрим, как это применимо. У нас есть 4 важных операции памяти, которые мы назовем оценками A, B, C и D:

Тема 1:

y.store (20);             //    Release; Evaluation A
x.store (10);             //    Release; Evaluation B

Тема 2:

if (x.load() == 10) {     //    Consume; Evaluation C
  assert (y.load() == 20) //    Consume; Evaluation D
  y.store (10)
}

Чтобы доказать, что assert никогда не срабатывает, мы фактически пытаемся доказать, что A всегда является видимым побочным эффектом в D. В соответствии с 5.1.2.4 (15) имеем:

A SeqB B DOB C SeqB D

который является конкатенацией, заканчивающейся DOB, а затем SeqB. Это явно управляется (17), чтобы не быть конкатенацией ITHB, несмотря на то, что говорит (16).

Мы знаем, что поскольку A и D не находятся в одном и том же потоке выполнения, A не является SeqB D; Следовательно, ни одно из двух условий в (18) для HB не выполняется, а A не HB D.

Отсюда следует, что A не виден D, так как одно из условий (19) не выполняется. Утверждение может потерпеть неудачу.


Как это могло бы произойти, тогда описывается здесь, в обсуждении модели стандартной модели С++ и здесь, раздел 4.2 "Зависимости управления" :

  • (Некоторое время вперед) Прогнозирование ветвления 2-го канала предполагает, что будет выполняться if.
  • Тема 2 приближается к предсказанной ветки и начинает спекулятивную выборку.
  • Резьба 2 вне порядка и спекулятивно загружает 0xGUNK из y (Оценка D). (Возможно, он еще не был выведен из кеша?).
  • Тема 1 хранит 20 в y (оценка A)
  • Тема 1 хранит 10 в x (оценка B)
  • Тема 2 загружает 10 из x (Оценка C)
  • Тема 2 подтверждает, что выполняется if.
  • Выполняется 2-я спекулятивная нагрузка y == 0xGUNK.
  • Ошибка потока 2.

Причина, по которой для оценки D допускается переупорядочивать до С, потому что потребление не запрещает ее. Это отличается от нагрузки на загрузку, которая предотвращает перегрузку любой загрузки/сохранения после того, как она была в порядке выполнения программы. Опять же, 5.1.2.4 (15) утверждает, что операция потребления, участвующая в "зависимой от зависимостей до отношений", обеспечивает упорядочение только по отношению к операциям, к которым эта операция потребления фактически несет зависимость, и там определенно не является зависимость между две нагрузки.


Проверка CppMem

CppMem - это инструмент, который помогает исследовать общие сценарии доступа к данным в моделях памяти C11 и С++ 11.

Для следующего кода, который аппроксимирует сценарий в вопросе:

int main() {
  atomic_int x, y;
  y.store(30, mo_seq_cst);
  {{{  { y.store(20, mo_release);
         x.store(10, mo_release); }
  ||| { r3 = x.load(mo_consume).readsvalue(10);
        r4 = y.load(mo_consume); }
  }}};
  return 0; }

Инструмент сообщает о двух последовательных сценариях без гонок, а именно:

Потребление, сценарий успеха

В котором y=20 успешно читается, и

Потребление, сценарий сбоев

В котором считывается "устаревшее" значение инициализации y=30. Свободный круг - мой.

В отличие от этого, когда mo_acquire используется для нагрузок, CppMem сообщает только о непротиворечивом сценарии один, а именно о правильном:

Приобретение, сценарий успеха

в котором читается y=20.

Ответ 2

Оба устанавливают транзитный "видимость" порядка в атомных хранилищах, если только они не были выпущены с помощью memory_order_relaxed. Если поток читает атомный объект x с одним из режимов, он может быть уверен, что он видит все модификации всех атомных объектов y, которые, как известно, были выполнены до записи в x.

Разница между "приобретать" и "потреблять" заключается в видимости неатомных записей для некоторой переменной z, скажем. Для acquire все записи, атомарные или нет, видны. Для consume гарантируется, что только атомные будут видны.

thread 1                               thread 2
z = 5 ... store(&x, 3, release) ...... load(&x, acquire) ... z == 5 // we know that z is written
z = 5 ... store(&x, 3, release) ...... load(&x, consume) ... z == ? // we may not have last value of z