我有一个带有原子成员的类,我想编写一个复制构造函数:
struct Foo
{
std::atomic<int> mInt;
Foo() {}
Foo(const Foo& pOther)
{
std::atomic_store(mInt, std::atomic_load(pOther.mInt, memory_order_relaxed), memory_order_relaxed);
}
};
但是我不知道我必须使用哪种顺序,因为我不知道这个拷贝构造函数将在何时何地被调用。
我可以对复制构造函数和赋值运算符使用relaxed
排序吗?
答案 0 :(得分:2)
不,如果您不知道如何使用它,您应该使用memory_order_seq_cst
来保证安全。如果您使用memory_order_relaxed
,则可能会遇到重新排序说明的问题。
答案 1 :(得分:1)
如果您的复制操作应与其他线程上的其他操作同步,则只需要比memory_order_relaxed
更强的内存排序。
但是,这几乎不是这种情况,因为线程安全的复制构造函数几乎总是需要一些外部同步或额外的互斥量。
答案 2 :(得分:1)
std::atomic<T>
模板删除其拷贝构造函数,因为原子是用于共享状态,因此将它们复制到另一个原子通常不是你想要的。
删除复制构造函数会强制类的用户考虑他们正在做什么,并记录他们正在执行一个值的原子加载,然后将该副本传递到其他位置。 (例如atomic<some_struct> var1 (var2.load())
)。见C++11: write move constructor with atomic<bool> member?
std::atomic<T>
is not itself atomic的构造函数,所以在构造函数中担心商店的顺序是没有意义的(除非你的构造函数调用了一堆其他函数并且放置了地址mInt
某个地方,另一个线程可以得到它...)
更好的是,使用复制的值作为初始化程序,而不是完全使用原子库。 (另见Nonlocking Way to Copy Atomics in Copy Constructor)。
我认为这可能是一个问题的唯一方法是,如果你正在做一些已经未定义的行为,比如使用placement-new
在已经共享的位置构建一个新的Foo
对象,当你这样做时,由其他线程读/写。这显然是疯了,所以不要这样做。
让你的类的内存排序行为匹配std::atomic<T>
的构造函数(即没有用于存储初始化程序)似乎是一个好主意。
只有调用者知道源操作数的加载是否需要顺序一致性。因此,您应该让调用者通过接受memory-order参数来选择,默认= seq_cst(与std::atomic
保持一致),而不是因为这是任何人可能想要的情况。是的,这是合法的C ++:copy constructor with default arguments
#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 compiler explorer)测试它,它在堆栈上构造一个,然后将其地址传递给非内联函数,该函数可能使该地址可供其他线程使用。所以它无法优化。
void extf(Foo &); // non-inline function
void test(const Foo *p) {
Foo tmp(*p);
extf(tmp);
}
无论extf()
做什么使地址可用于其他线程都应该使用release-store,这可以确保看到该地址的任何其他线程都能看到正确构造的Foo
。 这是一个正常的要求,这就是为什么初始化器甚至不是原子的完全正常。
请注意,作为单个原子操作(在C ++ 11中或在我所知的任何硬件上)不可能在两个不同的内存位置之间移动,因此强排序不太可能有用。 / p>
甚至定义这样的移动是否是原子的也是有问题的,因为原子性只存在于观察者眼中。由于不可能同时观察两个存储位置,因此这是一个毫无意义的概念。 (除非它们相邻,你可以用一个原子载荷得到它们。)