模板删除了它的std::atomic<T>
复制构造函数,因为原子用于共享状态,所以将它们复制到另一个原子通常不是你想要的。
删除复制构造函数会迫使您的类的用户考虑他们在做什么,并记录他们正在执行一个值的原子加载,然后将该副本传递到其他地方。(例如atomic<some_struct> var1 (var2.load())
)。请参阅C++11:使用 atomic<bool> 成员编写移动构造函数?
for 的构造函数std::atomic<T>
本身不是 atomic,因此在构造函数中担心存储的顺序是没有意义的(除非您的构造函数调用了一堆其他函数并将地址mInt
放在另一个线程可以获取它的某个位置.. .)
更好的是,使用复制的值作为初始化器,而不是进行原子存储。(另请参阅复制构造函数中复制原子的非锁定方式)。
我认为这可能是一个问题的唯一方法是,如果你正在做一些已经未定义的行为,比如使用placement-new
在一个已经共享的位置构造一个新Foo
对象,其他线程可以在你这样做的时候读取/写入. 这显然很疯狂,所以不要这样做。
让你的类的内存排序行为匹配std::atomic<T>
的构造函数(即没有用于存储初始化程序)似乎是个好主意。
只有调用者知道从源操作数加载是否需要顺序一致性。因此,您应该让调用者通过接受一个内存顺序参数来进行选择,默认 = seq_cst(为了与 保持一致std::atomic
,而不是因为在这种情况下任何人都可能想要)。是的,这是合法的 C++:带有默认参数的复制构造函数
#include <atomic>
struct Foo
{
std::atomic<int> mInt;
Foo() {}
Foo(const Foo& pOther, std::memory_order order = std::memory_order_seq_cst)
: mInt(pOther.mInt.load(order))
{}
};
这符合我预期的方式:对负载进行排序,但对存储没有排序。(例如,查看 ARM64 的 asm 输出显示负载用于ldar
执行获取加载,但存储只是一个简单的str
)。
我用这个调用者(Godbolt 编译器资源管理器)对其进行了测试,它在堆栈上构造一个,然后将其地址传递给一个非内联函数,该函数可能使该地址可用于其他线程。所以它无法优化。
void extf(Foo &); // non-inline function
void test(const Foo *p) {
Foo tmp(*p);
extf(tmp);
}
任何extf()
使该地址对其他线程可用的操作都应该使用 release-store,它确保看到该地址的任何其他线程都会看到一个正确构造的Foo
. 这是一个正常的要求,这就是为什么初始化器甚至不是原子的完全没问题。
请注意,作为单个原子操作(在 C++11 或我知道的任何硬件上)不可能在两个不同的内存位置之间进行移动,因此强排序不太可能有用。
甚至定义这样的移动是否是原子的也是有问题的,因为原子性只存在于观察者的眼中。由于不可能同时观察两个内存位置,因此这是一个毫无意义的概念。(除非它们是相邻的,并且您可以通过单个原子负载同时获得它们)。