Использование memory_order_relaxed для хранения с memory_order_acquire для загрузки
У меня есть вопрос, связанный со следующим кодом
#include <atomic>
#include <thread>
#include <assert.h>
std::atomic<bool> x, y;
std::atomic<int> z;
void write_x_then_y()
{
x.store(true, std::memory_order_relaxed);
y.store(true, std::memory_order_relaxed);
}
void read_y_then_x()
{
while (!y.load(std::memory_order_acquire));
if (x.load(std::memory_order_acquire))
++z;
}
int main()
{
x = false;
y = false;
z = 0;
std::thread a(write_x_then_y);
std::thread b(read_y_then_x);
a.join();
b.join();
assert(z.load() != 0);
}
Могу ли я быть уверен, что assert(z.load()!= 0) всегда ложно? Я думаю, что x.store и y.store не переупорядочены в потоке провайдера данных (это правда?). По этой причине я думаю, что если поток, значения загрузки которого хранятся в x и y, использует memory_order_acquire, он получал фактические значения для x и y из кэша ядра, который выполняет операторы хранения.
2 ответа
Я думаю, что утверждение может потерпеть неудачу. std::memory_order_relaxed
позволяет компилятору переупорядочивать магазины внутри write_x_then_y
, (Например, если он думает, что это будет быстрее по любой причине.) Так что он может написать y
до x
, Целый read_y_then_x
может произойти между этими двумя записями, и поэтому он будет наблюдать y
являющийся true
, x
являющийся false
и это не будет увеличиваться z
,
Хотя michalsrb уже ответил на него, я добавляю свой ответ, потому что он начал с "Я думаю";).
Модель памяти C++ допускает сбой утверждения.
Вот некоторые заметки:
- использование
#include <cassert>
; стандартные заголовки не заканчиваются на.h. atomic<bool>
а такжеatomic<int>
очень вероятно, что (без блокировки и) POD; определив их в глобальном пространстве имен, они будут инициализированы с изображением всех нулей; ака, они будут иметь значениеfalse
а также0
соответственно еще до достижения основной. Следовательно, три назначения в начале основного не имеют никакого эффекта. С другой стороны, в целом (например, когда атомарная переменная находится в стеке) существует разница между явной инициализацией и присваиванием: начальная инициализация неатомарна. Назначение превращается в хранилище с порядком памяти seq_cst. Поэтому лучшим стилем здесь будет (по-прежнему) использовать список инициализаторов (используя= false
и т.д. будет делать то же самое):std::atomic<bool> x{false}; std::atomic<bool> y{false}; std::atomic<int> z{0};
std::memory_order_acquire
вызывает синхронизацию только при чтении значения, записанного сstore memory_order_release
(который включает в себяmemory_order_seq_cst
это и релиз, и приобретение). Но так как у вас нетstore
сmemory_order_release
в другом потоке, безусловно, не произойдет никакой синхронизации. Инициализация вmain
был seq_cst, но это было сделано еще до того, как был создан поток b, поэтому там уже есть синхронизация (nl. Также-Синхронизируется-С, что очень похоже на взаимосвязь между потоками Sequenced-Before). Следовательно, используяstd::memory_order_relaxed
вместоstd::memory_order_acquire
будет делать то же самое, и использование явно memory_order_acquire кажется здесь немного странным.
Итак, поскольку нет никакой синхронизации между потоками a и b, нет никакой синхронизации между порядком, в котором оба потока видят изменения в x и y, и поток b может видеть, что y становится истинным, прежде чем он видит, что x становится истинным.
Не пытайтесь понять это с помощью переупорядочения компилятора, аппаратных конвейеров или чего-либо еще; это абстрактная модель памяти C++ "компьютер", которая не зависит от используемой вами реализации (компилятор) или аппаратного обеспечения. Это просто факт, что это изменение порядка допускается. Имея это в виду, поток b можно завершить и соединить, оставив z по-прежнему со значением 0.
Может быть поучительно посмотреть, что произойдет, если вы измените свою программу на:
void write_x_then_y()
{
x.store(true, std::memory_order_relaxed);
y.store(true, std::memory_order_release); // RELEASE HERE
}
void read_y_then_x()
{
while (!y.load(std::memory_order_acquire));
if (x.load(std::memory_order_relaxed))
++z;
}
Нить б еще будет висеть на while
пока он не читает значение true
для тебя. Таким образом, он читает значение, записанное потоком 1 с хранилищем memory_order_release
! Обратите внимание, что загрузка y все еще была сделана с memory_order_acquire
, Теперь происходит синхронизация: все, что было записано в любую область памяти (также не в y) перед сохранением / выпуском, из которого мы читаем, будет видно в потоке, который сделал чтение / получение после этого чтения. Другими словами, теперь x.store(true, std::memory_order_relaxed);
потока а будет виден в потоке b, когда он выполняет загрузку х; и утверждение никогда не подведет.