Приведет ли расслабленный порядок памяти к бесконечному циклу здесь?


Соответствующий код:

#include <atomic>
#include <thread>

std::atomic_bool stop(false);

void wait_on_stop() {
  while (!stop.load(std::memory_order_relaxed));
}

int main() {
  std::thread t(wait_on_stop);
  stop.store(true, std::memory_order_relaxed);
  t.join();
}
Поскольку здесь используется std::memory_order_relaxed, я предполагаю, что компилятор может свободно переупорядочивать stop.store() после t.join(). В результате, t.join() никогда не вернется. Правильно ли это рассуждение?

Если да, то изменение stop.store(true, std::memory_order_relaxed) на stop.store(true) решит проблему?

2 8

2 ответа:

[вступление.прогресс] / 18:

Реализация должна гарантировать, что последнее значение (в модификации порядок), назначенный атомарной операцией или операцией синхронизации, станет видимый для всех других потоков в течение конечного периода времени.

[атомика.порядок] / 12:

Реализации должны сделать атомарные хранилища видимыми для атомарных нагрузок в разумные сроки.

Это необязательная рекомендация. Если ваша реализация следует им - как и положено качественным реализациям-вы прекрасны. В противном случае, вы ввернуты. В обоих случаях, независимо от используемого порядка памяти.


Абстрактная машина C++ не имеет понятия "переупорядочивания". В абстрактной семантике основной поток хранится в атомарном, а затем блокируется, и поэтому, если реализация делает хранилище видимым для нагрузок в течение конечного промежутка времени, то другой поток загрузит это сохраненное значение в течение конечного промежутка времени и завершит работу. Напротив, если реализация не делает этого по какой-либо причине, то ваш другой поток будет петлять вечно. Используемый порядок памяти не имеет значения.

Я никогда не считал рассуждения о "переупорядочивании" полезными. Он смешивает низкоуровневые детали реализации с высокоуровневой моделью памяти и, как правило, делает вещи более запутанными, а не менее.

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

[вступление.исполнение]:

Чтение объекта, обозначенного изменчивым значением glvalue ([basic.lval]), изменение объекта, вызов функции ввода-вывода библиотеки или вызов функции, выполняющей любое из следующих действий: все эти операции являются побочными эффектами, то есть изменениями в состоянии среды выполнения. Оценка выражения (или подвыражения) в общем случае включает как вычисление значения (включая определение идентичности объекта для оценки glvalue и извлечение значения, ранее присвоенного объекту для оценки prvalue), так и инициирование побочных эффектов. Когда возвращается вызов функции ввода-вывода библиотеки или вычисляется доступ через изменчивое значение glvalue, побочным эффектом является считается завершенным, даже если некоторые внешние действия, подразумеваемые вызовом (например, сам ввод-вывод) или волатильным доступом, возможно, еще не завершены.

И

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

Здесь std::thread конструктор и std::thread::join являются такими функциями (они в конечном счете вызывают специфичные для платформы функции потока недоступны в текущем ту) с побочными эффектами. stop.store также вызывает побочные эффекты (хранилище памяти-это побочный эффект). Следовательно, stop.store не может быть перемещено до std::thread конструктора или после std::thread::join вызовов.