5

我有一个带有原子成员的类,我想编写一个复制构造函数:

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对复制构造函数和赋值运算符使用排序吗?

4

3 回答 3

3

不,如果你不知道如何使用它,你应该使用memory_order_seq_cst它以确保安全。如果您使用memory_order_relaxed,您可能会遇到重新排序指令的问题。

于 2013-11-13T18:23:12.837 回答
1

memory_order_relaxed如果您的复制操作应该与不同线程上的其他操作同步,您只需要比 更强的内存排序。
然而,这几乎从来不是这种情况,因为线程安全的复制构造函数几乎总是需要一些外部同步或额外的互斥锁。

于 2015-10-04T21:16:59.640 回答
1

模板删除了它的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 或我知道的任何硬件上)不可能在两个不同的内存位置之间进行移动,因此强排序不太可能有用。

甚至定义这样的移动是否是原子的也是有问题的,因为原子性只存在于观察者的眼中。由于不可能同时观察两个内存位置,因此这是一个毫无意义的概念。(除非它们是相邻的,并且您可以通过单个原子负载同时获得它们)。

于 2017-09-05T01:11:30.980 回答