Разница между 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 действительно может завершиться неудачей.
2 ответов
постановление стандарта C11 выглядит следующим образом.
5.1.2.4 многопоточные исполнения и гонки данных
оценка A является зависимость-заказал перед 16) оценка B, если:
- A выполняет операцию выпуска для атомарного объекта M, а в другом потоке B выполняет операцию потребления для M и считывает значение, записанное любым побочным эффектом в последовательности выпуска возглавляет или
- для некоторой оценки X A является зависимостью, упорядоченной до X и X несет зависимость от B.
оценка A Интер-поток происходит до оценка B если a синхронизируется с B, A является зависимостью, упорядоченной перед B, или, для некоторой оценки X:
- a синхронизируется с X и X секвенируется перед B,
- a секвенируется до того, как произойдет X и X inter-thread перед B или
- Интер-поток происходит до X и X Интер-поток происходит до B.
Примечание 7 отношение’ inter-thread происходит до "описывает произвольные конкатенации отношений’ sequenced before‘,’ synchronizes with ‘и’ dependency-ordered before",С двумя исключениями. Первым исключением является то, что конкатенация не разрешается заканчиваться ‘зависимостью-упорядоченной до’, за которой следует "sequenced before". Причина этого ограничения заключается в том, что операция потребления, участвующая в связи ‘зависимость упорядочена до’, обеспечивает упорядочение только в отношении операций, к которым эта операция потребления фактически несет зависимость. причина, по которой это ограничение применяется только к концу такой конкатенации, заключается в том, что любая последующая операция выпуска обеспечит необходимый заказ для предыдущей операции потребления. Второе исключение состоит в том, что конкатенация не разрешается полностью состоять из "sequenced before". Причины этого ограничения: (1) разрешить ‘Интер-поток происходит до’ быть транзитивно закрытым и (2) отношение ‘происходит до’, определенное ниже, предусматривает отношения, полностью состоящие из ‘последовательности до’.
оценка A происходит перед оценка B, если a секвенируется до B или Интер-поток случается раньше Б.
A видимый побочный эффект, а на объекте M относительно вычисления значения B из M удовлетворяет условиям:
- A происходит до B, и
- нет другого побочного эффекта X для M, такого, что A происходит до X и X происходит до B.
значением неатомного скалярного объекта M, определяемым оценкой B, является значение, сохраненное видимой стороной А. Влияние
(курсив)
в комментарии ниже, я сокращу ниже:
- зависимость-заказано до: дата рождения
- Интер-поток происходит раньше: 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)
}
чтобы доказать утверждение никогда не экскурсии, мы в действительности стремимся доказать, что A всегда является видимым побочным эффектом при D. В соответствии с 5.1.2.4 (15), имеем:
A SeqB B дата рождения C SeqB D
который является конкатенацией, заканчивающейся на DOB с последующим SeqB. Это явно управляется (17) до не быть конкатенацией ITHB, несмотря на то, что говорит (16).
мы знаем, что, поскольку A и D не находятся в одном потоке выполнения, A не является SeqB D; следовательно, ни один из двух условия в (18) для HB выполнены, а A не HB D.
из этого следует, что A не виден D, так как одно из условий (19) не выполняется. Утверждение может потерпеть неудачу.
как это могло произойти, тогда описано здесь, в обсуждении модели памяти стандарта C++ и здесь, Раздел 4.2 зависимости управления:
- (некоторое время вперед) предсказатель ветви потока 2 догадывается, что
if
будут приняты. - поток 2 приближается к предсказанной ветви и начинает спекулятивную выборку.
- поток 2 не в порядке и спекулятивно загружает
0xGUNK
Сy
(Оценка D). (Может быть, его еще не выселили из тайника?). - поток 1 магазины
20
наy
(Оценка A) - поток 1 магазины
10
наx
(Оценка B) - поток 2 нагрузки
10
Сx
(оценка C) - поток 2 подтверждает - это.
- спекулятивная нагрузка потока 2
y == 0xGUNK
помогут. - поток 2 не утверждать.
причина, по которой разрешено переупорядочивать оценку D перед C, заключается в том, что a потреблять тут не запрещаю. Это не похоже на приобрести-нагрузка, который предотвращает любую нагрузку / магазин после это в порядке программы от переупорядочивания до его. Опять же, в пункте 5.1.2.4 (15) говорится:операция потребления, участвующая в отношениях’ зависимость-упорядочена до", обеспечивает упорядочение только в отношении операций, к которым эта операция потребления фактически несет зависимость, и там определенно нет зависимости между двумя нагрузками.
проверка CppMem
CppMem это инструмент, который помогает исследовать общие сценарии доступа к данным в модели памяти C11 и c++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
читать. Freehand круг-моя.
напротив, когда mo_acquire
используется для нагрузок, cppmem сообщает только один последовательный, свободный от гонки сценарий, а именно правильный:
, в котором y=20
читать.
оба устанавливают транзитивный порядок "видимости" на атомных хранилищах, если они не были выпущены с 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