Следующий код записывает в атомарные переменные A, B и читает их в обратном порядке в другом потоке:
#include <atomic>
#include <thread>
// Initially.
std::atomic<int> A = std::atomic<int>(0);
std::atomic<int> B = std::atomic<int>(0);
// Thread 1
void write()
{
A.store(1, std::memory_order_release); // line 1
B.store(1, std::memory_order_release); // line 2
}
// Thread 2
void read()
{
int valB = B.load(std::memory_order_acquire); // line 3
int valA = A.load(std::memory_order_acquire); // line 4
}
int main()
{
std::thread t1(write), t2(read);
t1.join();
t2.join();
return 0;
}
В идеале, учитывая порядок выпуска-получения на атоме B, загрузка в строке 3 должна синхронизироваться с завершением сохранения в строке 1, так как это происходит до операции выпуска в строке 2. Но ссылка упоминает только (выделение , мой) -
Все операции записи в память (неатомарные и ослабленные атомарные), которые происходили до атомарного сохранения с точки зрения потока A, становятся видимыми побочными эффектами в потоке B.
Итак, порядок выпуска-приобретения для B не влияет на атомарные операции для других переменных, которые не являются ослабленным порядком памяти?
Возможно ли тогда иметь значения для valB = 1
, valA = 0
после завершения read() в потоке 2, поскольку порядок памяти B не заботится о других атомарных операциях, которые не ослаблены? Нужна ли нам здесь последовательная согласованность?
Большой! Всегда лучше использовать реальный пример компиляции, написанный на языке, о котором вы спрашиваете.
Чтобы не придираться к примеру, но… он по-прежнему в корне ошибочен — создание потока само по себе является точкой синхронизации. Если вы спрашиваете о «потоке 1» и «потоке 2», просто создайте 2 потока; вызов write()
в основной теме на самом деле не короче и определенно меняет характер вопроса (непреднамеренно).
@ildjarn что именно вы подразумеваете под созданием потока, это точка синхронизации? Не могли бы вы уточнить, в контексте текущего примера. Спасибо
@Akash: Я бы не хотел, поэтому я написал комментарий, а не ответ. Пожалуйста, обратитесь к документации для конструктора std::thread
, в которой указано, что завершение конструктора thread
синхронизируется с началом вызова предоставленного вызываемого объекта в новом потоке. Таким образом, в показанном здесь коде работающий поток read
совершенно не может видеть ничего, кроме 1
и 1
, но не по причинам, о которых вы спрашиваете.
@ildjarn Я понял твою точку зрения. Обновленный вопрос. Спасибо!
Возможно ли тогда иметь значения для valB = 1, valA = 0 после завершения read() в потоке 2, поскольку порядок памяти B не заботится о других атомарных операциях, которые не ослаблены?
Нет, это невозможно. Как вы процитировали:
Все операции записи в память (неатомарные и ослабленные атомарные), которые происходили до атомарного сохранения с точки зрения потока A, становятся видимыми побочными эффектами в потоке B.
Таким образом, когда вы читаете 1
из B
, то, что произошло до записи в A
, видно в потоке чтения, и, следовательно, следующее чтение из A
также должно возвращать 1
.
Выделенную вами часть в скобках можно было бы лучше сформулировать как «(включая неатомарные и расслабленно-атомарные)» — это то, что вас сбило с толку?
Соответствующей частью стандарта будет atomics.order.1.4:
Атомарная операция A, которая выполняет операцию освобождения атомарного объекта M, синхронизируется с атомарной операцией B, которая выполняет операцию получения M.
Значение «синхронизируется с» описано в intro.multithread:
Оценка A между потоками происходит до оценки B, если A синхронизируется [...]
Оценка A происходит до оценки B (или, что то же самое, B происходит после A), если
- A расположен перед B, или
- Интерпоток происходит до B.
Происходит-перед является транзитивным: A.store()
происходит-до (последовательность до) B.store()
, происходит-до (синхронизируется-с) B.load()
, происходит-до (последовательность до) A.load()
. Таким образом, A.store()
происходит раньше A.load()
.
В стандарте прямо говорится об атомарных объектах:
Если побочный эффект X на атомарном объекте M возникает до вычисления значения B для M, то оценка B должна брать свое значение из X или из побочного эффекта Y, который следует за X в порядке модификации M.
Итак, поскольку A.store(1)
происходит до A.load()
и нет других операций-кандидатов Y, A.load()
должна возвращать 1.
Я также могу порекомендовать этот пост в блоге.
Да, в ссылке упоминаются только неатомарные операции, и, расслабленные атомарные операции, значит, я что-то неправильно читаю?
Я предполагаю, что автор предположил, что это очевидно для атомарных операций, и ему не нужно было их перечислять. Обратите внимание, что фактическое предложение по-прежнему говорит все. Позвольте мне посмотреть, смогу ли я найти для вас более авторитетный источник.
Дополнил свой ответ соответствующими цитатами из стандарта, в которых это четко указано.
@Akash: Текст на cppreference неправильный/был неправильным и не говорит о том, что автор, вероятно, имел в виду (как говорит Chronial, они, вероятно, имели в виду «включая», но это не то, что они написали). Если бы B.store
был relaxed
, более ранний магазин release
мог бы изменить заказ с ним, так что это на самом деле важное различие. (Даже более раннему хранилищу seq_cst будет разрешено переупорядочиваться, и это возможно при компиляции для AArch64.) (Модель памяти C++ формально определяется с точки зрения создания отношений «происходит до», а не с точки зрения локального переупорядочивания доступов к когерентному общему кешу, например мы обычно думаем.)
Зафиксированный. Это должно было объяснить логику с помощью псевдокода. Я не думаю, что минимальный воспроизводимый пример помог бы здесь.