Ответ 1
Ваше использование фактически не гарантирует того, что вы упомянули в своих комментариях. То есть ваше использование забора не гарантирует, что ваши назначения a
видны другим потокам или что значение, которое вы читаете с a
, является "актуальным". Это связано с тем, что, хотя у вас, похоже, есть основная идея того, где следует использовать ограждения, ваш код на самом деле не соответствует точным требованиям для этих ограждений, чтобы "синхронизировать".
Вот другой пример, который, я думаю, лучше показывает правильное использование.
#include <iostream>
#include <atomic>
#include <thread>
std::atomic<bool> flag(false);
int a;
void func1()
{
a = 100;
atomic_thread_fence(std::memory_order_release);
flag.store(true, std::memory_order_relaxed);
}
void func2()
{
while(!flag.load(std::memory_order_relaxed))
;
atomic_thread_fence(std::memory_order_acquire);
std::cout << a << '\n'; // guaranteed to print 100
}
int main()
{
std::thread t1 (func1);
std::thread t2 (func2);
t1.join(); t2.join();
}
Загрузка и сохранение на атомном флаге не синхронизируются, потому что они оба используют упорядоченное упорядочение памяти. Без ограждений этот код будет представлять собой гонку данных, потому что мы выполняем конфликтующие операции неатомным объектом в разных потоках, и без заборов и синхронизации, которые они обеспечивают, не будет происходить - до отношения между конфликтующими операциями на a
.
Однако с забором мы получаем синхронизацию, потому что мы гарантировали, что поток 2 прочитает флаг, написанный потоком 1 (потому что мы зацикливаемся до тех пор, пока не увидим это значение), и так как атомная запись произошла после забора релиза и происходит атомарное считывание - до получения забора, заграждения синхронизируются. (см. § 29.8/2 для конкретных требований.)
Эта синхронизация означает все, что происходит - до того, как произойдет освобождение забора - перед чем-либо, что происходит - после забора. Поэтому неатомная запись в a
происходит до неатомного чтения a
.
Все становится сложнее, когда вы пишете переменную в цикле, потому что вы можете установить связь между случаями и событиями для какой-либо конкретной итерации, но не другие итерации, вызывающие гонку данных.
std::atomic<int> f(0);
int a;
void func1()
{
for (int i = 0; i<1000000; ++i) {
a = i;
atomic_thread_fence(std::memory_order_release);
f.store(i, std::memory_order_relaxed);
}
}
void func2()
{
int prev_value = 0;
while (prev_value < 1000000) {
while (true) {
int new_val = f.load(std::memory_order_relaxed);
if (prev_val < new_val) {
prev_val = new_val;
break;
}
}
atomic_thread_fence(std::memory_order_acquire);
std::cout << a << '\n';
}
}
Этот код по-прежнему заставляет ограждения синхронизировать, но не устраняет расы данных. Например, если f.load()
происходит с возвратом 10, то мы знаем, что a=1
, a=2
,... a=10
все произошло - до этого конкретного cout<<a
, но мы не знаем, что cout<<a
происходит до a=11
. Это конфликтующие операции на разных потоках без каких-либо связей; гонка данных.