【问题标题】:Copy constructor for classes with atomic member具有原子成员的类的复制构造函数
【发布时间】:2013-11-13 17:59:54
【问题描述】:

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

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 排序吗?

【问题讨论】:

  • 提供复制构造函数,而是提供一个从用户那里获取内存顺序的函数呢? (同样适用于作业)
  • @DavidRodríguez-dribeas 我不能这样做,例如我想使用这个类作为另一个类的模板参数,而那个类只调用复制构造函数
  • 标准原子类型没有复制构造函数是有原因的...如果您需要这个,那么唯一明智的做法就是使用顺序一致性,较慢但安全。任何其他选择,您最终可能会得到不需要的数据。

标签: c++ multithreading atomic


【解决方案1】:

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

【讨论】:

  • 来自现有Foo 的加载应该默认为seq_cst,但原子对象的构造函数需要对其存储进行强排序是没有意义的。任何使新构造对象的地址对其他线程可用的任何东西都需要使用释放存储来确保构造函数在另一个线程可以使用它之前完成。请参阅我的答案:将加载的值作为初始化程序传递,而不是使用存储。
【解决方案2】:

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

【讨论】:

    【解决方案3】:

    std::atomic&lt;T&gt; 模板删除了它的复制构造函数,因为原子用于共享状态,所以将它们复制到另一个原子通常不是你想要的。

    删除复制构造函数会迫使您的类的用户思考他们在做什么,并记录他们正在执行一个值的原子加载,然后将该副本传递到其他地方。 (例如atomic&lt;some_struct&gt; var1 (var2.load()))。见C++11: write move constructor with atomic<bool> member?


    std::atomic&lt;T&gt;is not itself atomic 的构造函数,所以在你的构造函数中担心存储的顺序是没有意义的(除非你的构造函数调用了一堆其他函数并将mInt 的地址放在某个地方另一个线程可以得到它...)

    更好的是,使用复制的值作为初始值设定项,而不是进行原子存储。 (另见Nonlocking Way to Copy Atomics in Copy Constructor)。

    我认为这可能是一个问题的唯一方法是,如果您正在做一些已经未定义的行为,例如使用 placement-new 在可以读取/写入的已共享位置构造一个新的 Foo 对象当你这样做时,其他线程。这显然很疯狂,所以不要这样做。

    让你的类的内存排序行为匹配 std::atomic&lt;T&gt; 的构造函数(即没有用于存储初始化程序)似乎是个好主意。


    只有调用者知道从源操作数加载是否需要顺序一致性。因此,您应该让调用者通过接受一个内存顺序参数来选择,默认 = 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() 为使该地址对其他线程可用所做的任何事情,都应使用释放存储,以确保看到该地址的任何其他线程将看到正确构造的Foo这是一个正常的要求,这就是为什么初始化器甚至不是原子的完全没问题。


    请注意,作为单个原子操作(在 C++11 或我知道的任何硬件上)不可能在两个不同的内存位置之间进行移动,因此强排序不太可能有用。

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

    【讨论】:

    • 摩托罗拉 680x0 系列有一段时间有一个 CAS2 或(又名 DCAS),它可以让您以原子方式对两个不连续的内存位置进行 CAS。我认为他们后来摆脱了它,因为显然很难有效地实现它。在某种程度上,HTM 是现代的替代品。
    猜你喜欢
    • 2021-04-19
    • 2014-04-15
    • 1970-01-01
    • 2015-08-10
    • 2015-08-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多