考虑以下伪代码:
expected = null;
if (variable == expected)
{
atomic_compare_exchange_strong(
&variable, expected, desired(), memory_order_acq_rel, memory_order_acq);
}
return variable;
请注意,执行variable == expected
检查时,没有"获取"语义。
在我看来,desired
总共至少会被调用一次,每个线程最多调用一次
此外,如果desired
从不返回null
,,则此代码将从不返回null
。
现在,我有三个问题:
以上情况必然成立吗?即,即使在每次读取都没有围栏的情况下,我们真的能对共享变量进行有序的读取吗?
有可能在C++中实现这一点吗?如果是,如何?如果没有,为什么
(希望有一个理由,而不仅仅是"因为标准这么说"。(如果(2(的答案是肯定的,那么是否也可以在C++中实现这一点,而不需要
variable == expected
执行对variable
的原子读取?
基本上,我的目标是了解,一旦每个线程至少执行了一次代码,是否可以以与非共享变量性能相同的方式执行共享变量的延迟初始化?
(这在某种程度上是一个"语言律师"的问题。因此,这意味着问题不是关于这是一个好主意还是有用的主意,而是关于在技术上是否可以正确地做到这一点。(
关于是否可以在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;
}
这是未定义的行为。您正在修改variable
,位于在某些线程中最少,这意味着所有访问变量必须受到保护。尤其是当你在一个线程中执行CCD_ 10,没有什么可以保证另一个线程可能会看到variable
的新值,然后再看到可能发生在CCD_ 12中。(atomic_compare_exchange_strong
仅保证执行它的线程中的任何排序。(