Как на самом деле работают заборы в C++

Я изо всех сил пытался понять, как заборы на самом деле заставляют код синхронизироваться.

например, скажем, у меня есть этот код

bool x = false;
std::atomic<bool> y;
std::atomic<int> z;
void write_x_then_y()
{
    x = true;
    std::atomic_thread_fence(std::memory_order_release);
    y.store(true, std::memory_order_relaxed);
}
void read_y_then_x()
{
    while (!y.load(std::memory_order_relaxed));
    std::atomic_thread_fence(std::memory_order_acquire);
    if (x)
        ++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);
}

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

но если бы y не была такой атомарной переменной

bool x;
bool y;
std::atomic<int> z;
void write_x_then_y()
{
    x = true;
    std::atomic_thread_fence(std::memory_order_release);
    y = true;
}
void read_y_then_x()
{
    while (!y);
    std::atomic_thread_fence(std::memory_order_acquire);
    if (x)
        ++z;
}

тогда, как я слышал, может начаться гонка данных. Но почему? Почему за блоками освобождения должно следовать атомарное хранилище, а за блоками получения должна предшествовать атомарная загрузка, чтобы код правильно синхронизировался?

Я также был бы признателен, если бы кто-нибудь мог предоставить сценарий выполнения, в котором гонка данных вызывает срабатывание утверждения.


person GamefanA    schedule 09.12.2015    source источник
comment
Останавливаться. Посмотри это видео. Посмотрите еще раз, а затем примите, что указание порядка использования памяти — опасная трата вашего времени: channel9.msdn.com/Shows/Going+Deep/   -  person Richard Hodges    schedule 10.12.2015
comment
Заборы никак не связаны с тем, что неупорядоченный одновременный доступ к неатомарным переменным — это UB согласно модели памяти. Заборы обеспечивают синхронизацию по правильному коду, но не препятствуют UB.   -  person Kerrek SB    schedule 10.12.2015
comment
Честно говоря, лучший ответ на этот вопрос, наверное, таков, потому что так говорят правила, а системы могут взламывать код, который не соответствует правилам.   -  person David Schwartz    schedule 10.12.2015
comment
очень хорошее видео @RichardHodges. это помогло объяснить большую часть семантики получения и выпуска, о которой я не знал. Обычно я никогда не указываю порядок памяти (я просто использую последовательную согласованную модель по умолчанию :), но мне было просто любопытно, почему такая странная реализация необходима для того, чтобы приведенный выше код не имел гонки данных.   -  person GamefanA    schedule 10.12.2015
comment
@user3769877 user3769877 Я действительно считаю, что презентация должна быть обязательной для просмотра всем, кто собирается писать многопоточный код.   -  person Richard Hodges    schedule 10.12.2015


Ответы (1)


Никакая настоящая гонка данных не является проблемой для вашего второго фрагмента. Этот фрагмент был бы в порядке... если бы компилятор буквально генерировал машинный код из написанного.

Но компилятор может сгенерировать любой машинный код, эквивалентный исходному в случае однопоточной программы.

Например, компилятор может отметить, что переменная y не изменяется в цикле while(!y), поэтому он может загрузить эту переменную один раз для регистрации и использовать только этот регистр в следующих итерациях. Итак, если изначально y=false, вы получите бесконечный цикл.

Другая возможная оптимизация — это просто удаление цикла while(!y), так как он не содержит обращений к переменным volatile или atomic и не использует synchronization< /em> действия. (Стандарт C++ говорит, что любая правильная программа должна в конце концов выполнить одно из указанных выше действий, поэтому компилятор может полагаться на этот факт при оптимизации программы).

И так далее.

В более общем плане стандарт C++ указывает, что одновременный доступ к любой неатомарной переменной приводит к неопределенному поведению, например, "Гарантия снята" . Вот почему вы должны использовать переменную atomic y.

С другой стороны, переменная x не обязательно должна быть атомарной, так как доступ к ней не является параллельным из-за ограничений памяти.

person Tsyvarev    schedule 09.12.2015