Использование 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, когда он выполняет загрузку х; и утверждение никогда не подведет.

Другие вопросы по тегам