考虑以下伪代码:
expected = null;
if (variable == expected)
{
atomic_compare_exchange_strong(
&variable, expected, desired(), memory_order_acq_rel, memory_order_acq);
}
return variable;
执行variable == expected
检查时,观察 no “获取”语义。
在我看来,desired
至少会被调用一次,每个线程最多一次。
此外,如果desired
永远不会返回null
,那么此代码永远不会返回null
。
现在,我有三个问题:
以上一定是真的吗?也就是说,即使在每次阅读时没有围栏,我们是否真的可以对共享变量进行有序读取?
是否可以在C ++中实现它?如果是这样,怎么样?如果没有,为什么?
(希望有一个基本原理,而不仅仅是“因为标准这么说”。)
如果(2)的答案是肯定的,那么是否也可以在C ++ 中实现这一点而不需要要求variable == expected
执行 atomic 阅读variable
?
基本上,我的目标是了解是否有可能以一种代码已经具有与非共享变量相同的方式执行共享变量的延迟初始化每个线程至少执行一次?
(这有点像“语言 - 律师”问题。所以这意味着问题不在于这是一个好的还是有用的想法,而是在技术上是否可以正确地做到这一点。)
答案 0 :(得分:4)
关于是否可以在C ++中执行共享变量的延迟初始化的问题,其性能(几乎)与非共享变量的性能(几乎)相同:
答案是,它取决于硬件架构,以及编译器和运行时环境的实现。至少,在某些环境中是可能的。尤其是在带有GCC和Clang的x86上。
在x86上,可以在没有内存防护的情况下实现原子读取。基本上,原子读取与非原子读取相同。看看下面的编译单元:
std::atomic<int> global_value;
int load_global_value() { return global_value.load(std::memory_order_seq_cst); }
虽然我使用顺序一致性(默认值)的原子操作,但生成的代码没有什么特别之处。 GCC和Clang生成的汇编程序代码如下:
load_global_value():
movl global_value(%rip), %eax
retq
我说几乎相同,因为还有其他可能影响性能的原因。例如:
话虽如此,实现延迟初始化的推荐方法是使用std::call_once
。这应该为所有编译器,环境和目标架构提供最佳结果。
std::once_flag _init;
std::unique_ptr<gadget> _gadget;
auto get_gadget() -> gadget&
{
std::call_once(_init, [this] { _gadget.reset(new gadget{...}); });
return *_gadget;
}
答案 1 :(得分:3)
这是未定义的行为。您正在修改variable
,at
至少在某个线程中,这意味着所有访问
变量必须受到保护。特别是,当你做的时候
在一个线程中执行atomic_compare_exchange_strong
,
没有什么可以保证另一个线程可能会看到
variable
的新值在看到可能的写入之前
发生在desired()
。 (atomic_compare_exchange_strong
只保证执行它的线程中的任何排序。)