std::atomic<bool> b;
void f()
{
// block A
if(b.load(std::memory_order_relaxed))
{
// block B
}
// block C
}
void g()
{
// block B
b.store(true, std::memory_order_release);
}
理论上,仅当原子载荷返回true时才执行块B,
但是是否有可能在加载之前对块B的一部分进行重新排序? store
具有释放内存顺序,可以保证对块B的所有操作都是可见的副作用,但是如果load
是宽松的操作,这仍然适用吗?
答案 0 :(得分:3)
英特尔在Benefitting Power and Performance Sleep Loops中建议在尝试锁定之前进行轻松的加载:
ATTEMPT_AGAIN:
if (!acquire_lock())
{
/* Spin on pause max_spin_count times before backing off to sleep */
for(int j = 0; j < max_spin_count; ++j)
{
/* pause intrinsic */
_mm_pause();
if (read_volatile_lock()) // <--- relaxed load
{
if (acquire_lock())
{
goto PROTECTED_CODE;
}
}
}
/* Pause loop didn't work, sleep now */
Sleep(0);
goto ATTEMPT_AGAIN;
}
PROTECTED_CODE:
get_work();
release_lock();
do_work();
acquire_lock
使用获取语义,以使宽松的负载不会在acquire_lock
之后重新排序。
但是,请注意,它首先尝试无条件地锁定,然后使用轻松的负载执行忙等待循环。
答案 1 :(得分:3)
您的示例中有两个block B
。我说的是void f()
加载函数中的那个。
是否有可能在加载之前对B块的一部分进行重新排序?
是的。编译器可以将负载从if()
主体中吊起,然后在b.load
之前进行。如果B块和C块都读取相同的非原子变量,则可能会发生这种情况。
即使没有编译时重新排序,现实生活中的机制也会创建这种重新排序:
具体来说,分支推测(即分支预测+乱序推测执行)将使CPU在b.load()
甚至开始之前就开始执行块B。
您不能依赖“因果关系”或诸如“必须知道b.load()
结果才能知道下一步要执行什么”之类的推理。
或者如果块B中没有任何存储,编译器有可能将if()
转换为无分支代码。那么很显然,它可以很明显地以非原子负载,或其他宽松的或获得B和C区块中的负载。
(记住acq / rel是单向障碍。)
这样的推理(基于实际的编译器和CPU的能力)有助于证明不安全。 但是要注意另辟:径:基于“我知道的编译器上的安全性”进行推理并不总是意味着“在可移植ISO C ++中是安全的” 。
有时候“对我所了解的编译器是安全的”或多或少是足够的,但是很难将其与“对我所了解的编译器进行工作的机会”区分开来,因为将来可能会出现编译器版本或看似无关的源代码更改弄碎东西。
因此,请始终尝试根据C ++内存模型以及如何针对您关心的ISA有效地进行编译(例如强排序的x86)来对内存排序进行推理。就像您可能会看到的那样,放宽将允许在您的情况下实际有用的编译时重新排序。
答案 2 :(得分:1)
您应该关注的主要原则是访问使用此“互斥体”锁定的资源。没有获取/释放语义,您的线程可能看不到其他线程对该资源所做的更改。也就是说,您从该数据中读取数据以及另一线程对该数据的写入构成了没有获取/释放语义的数据竞争。
如果您只想访问原子值本身,而又不问世界上与该原子值有关的其他问题,则只能使用宽松的内存顺序。 >