栅栏实际上如何在 C++ 中工作
How do fences actually work in 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;
}
然后,我听说,可能会出现数据竞争。但这是为什么呢?
为什么必须在释放栅栏之后进行原子存储,并且在获取栅栏之前进行原子加载才能使代码正确同步?
如果有人能提供一个数据竞争导致断言触发的执行场景,我将不胜感激
您的第二个代码段没有真正的数据竞争问题。如果编译器字面上从编写的代码中生成机器码,这个片段就可以了。
但是编译器可以自由生成任何机器码,在单线程程序.
的情况下相当于原始机器码
例如,编译器可以注意到,y
变量在 while(!y)
循环中不会改变,因此它可以加载此变量一次以进行注册,并在下一次迭代中仅使用该寄存器。所以,如果最初 y=false
,你会得到一个无限循环。
另一个可能的优化是删除 while(!y)
循环,因为它不包含对 volatile 或 atomic[ 的访问=42=] 变量并且不使用 同步 操作。 (C++ 标准说任何正确的程序都应该最终 执行上面指定的操作之一,因此编译器在优化程序时可能会依赖于该事实)。
以此类推
更一般地说,C++ 标准指定 并发 访问任何 非原子 变量会导致 未定义的行为,类似于 "Warranty is cleared"。这就是为什么你应该使用 atomic y
变量。
从另一方面来说,变量 x
不需要是原子的,因为由于内存栅栏,对它的访问不是并发的。
我一直在努力理解栅栏实际上是如何强制代码同步的。
例如,假设我有这个代码
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;
}
然后,我听说,可能会出现数据竞争。但这是为什么呢? 为什么必须在释放栅栏之后进行原子存储,并且在获取栅栏之前进行原子加载才能使代码正确同步?
如果有人能提供一个数据竞争导致断言触发的执行场景,我将不胜感激
您的第二个代码段没有真正的数据竞争问题。如果编译器字面上从编写的代码中生成机器码,这个片段就可以了。
但是编译器可以自由生成任何机器码,在单线程程序.
的情况下相当于原始机器码例如,编译器可以注意到,y
变量在 while(!y)
循环中不会改变,因此它可以加载此变量一次以进行注册,并在下一次迭代中仅使用该寄存器。所以,如果最初 y=false
,你会得到一个无限循环。
另一个可能的优化是删除 while(!y)
循环,因为它不包含对 volatile 或 atomic[ 的访问=42=] 变量并且不使用 同步 操作。 (C++ 标准说任何正确的程序都应该最终 执行上面指定的操作之一,因此编译器在优化程序时可能会依赖于该事实)。
以此类推
更一般地说,C++ 标准指定 并发 访问任何 非原子 变量会导致 未定义的行为,类似于 "Warranty is cleared"。这就是为什么你应该使用 atomic y
变量。
从另一方面来说,变量 x
不需要是原子的,因为由于内存栅栏,对它的访问不是并发的。