使用atomic锁定自由单个生成器多个使用者数据结构

时间:2016-10-10 10:27:32

标签: c++ c++14 lock-free stdatomic

我最近有类似下面的示例代码(实际代码要复杂得多)。看过Hans Boehm的cppcon16关于原子的讨论后,我有点担心我的代码是否有效。

produce由单个生产者线程调用,consume由多个使用者线程调用。生产者只更新序列号中的数据,如2,4,6,8,...,但在更新数据之前设置为奇数序列号,如1,3,5,7 ......,以指示数据可能是脏的。消费者也试图以相同的顺序(2,4,6,...)获取数据。

Consumer read会在读取后检查序列号,以确保数据良好(读取时不会被生产者更新)。

我认为我的代码在x86_64(我的目标平台)上工作正常,因为x86_64不会将商店与其他商店重新排序,或者加载商店或加载,但我怀疑它在其他平台上是错误的。

我是否正确,数据分配(在产品中)可以移动到' store(n-1)'消费者读取损坏的数据,但t == t2仍然成功吗?

struct S 
{
    atomic<int64_t> seq;
    // data members of primitive type int, double etc    
    ...
};

S s;

void produce(int64_t n, ...) // ... for above data members
{
    s.seq.store(n-1, std::memory_order_release); // indicates it's working on data members

    // assign data members of s
    ...

    s.seq.store(n, std::memory_order_release); // complete updating
}

bool consume(int64_t n, ...) // ... for interested fields passed as reference
{
    auto t = s.load(std::memory_order_acquire);

    if (t == n)
    {
        // read fields
        ...

        auto t2 = s.load(std::memory_order_acquire);
        if (t == t2)
            return true;
    }        

    return false;
}

1 个答案:

答案 0 :(得分:3)

在定位x86时,

Compile-time reordering仍然可以咬你,因为编译器会优化以保留C ++抽象机器上程序的行为,而不是任何更强大的架构相关行为。由于我们要避免memory_order_seq_cst,因此允许重新排序。

是的,您的商店可以按照您的建议重新排序。由于an acquire-load is only a one-way barrier,您的加载也可以使用t2加载重新排序。编译器完全优化t2检查是合法的。如果可以重新排序,则允许编译器确定始终发生的是什么,并应用as-if规则来生成更高效的代码。 (目前的编译器通常不会这样做,但现在的标准绝对允许这样做。请参阅the conclusion of a discussion about this, with links to standards proposals。)

您防止重新排序的选项是:

  • 使所有数据成员存储/加载原子化并释放并获取语义。 (最后一个数据成员的获取负载将使t2负载首先完成。)
  • 使用barriers (aka fences)将所有非原子存储和非原子加载作为一个整体排序。

    Jeff Preshing解释说,a mo_release fence isn't the same thing as a mo_release store,是我们需要的双向障碍。 std :: atomic只是回收std :: mo_名称,而不是为围栏指定不同的名称。

    (顺便说一句,非原子存储/加载应该是mo_relaxed的原子,因为技术上未定义的行为在它们可能正在被重写的过程中完全读取它们,即使你决定不看看你读到了什么。)

void produce(int64_t n, ...) // ... for above data members
{
    /*********** changed lines ************/
    std::atomic_signal_fence(std::memory_order_release);  // compiler-barrier to make sure the compiler does the seq store as late as possible (to give the reader more time with it valid).
    s.seq.store(n-1, std::memory_order_relaxed);          // changed from release
    std::atomic_thread_fence(std::memory_order_release);  // StoreStore barrier prevents reordering of the above store with any below stores.  (It's also a LoadStore barrier)
    /*********** end of changes ***********/

    // assign data members of s
    ...

    // release semantics prevent any preceding stores from being delayed past here
    s.seq.store(n, std::memory_order_release); // complete updating
}



bool consume(int64_t n, ...) // ... for interested fields passed as reference
{
    if (n == s.seq.load(std::memory_order_acquire))
    {
        // acquire semantics prevent any reordering with following loads

        // read fields
        ...

    /*********** changed lines ************/
        std::atomic_thread_fence(std::memory_order_acquire);  // LoadLoad barrier (and LoadStore)
        auto t2 = s.seq.load(std::memory_order_relaxed);    // relaxed: it's ordered by the fence and doesn't need anything extra
        // std::atomic_signal_fence(std::memory_order_acquire);  // compiler barrier: probably not useful on the load side.
    /*********** end of changes ***********/
        if (n == t2)
            return true;
    }

    return false;
}

注意额外的编译器屏障(signal_fence仅影响编译时重新排序)以确保编译器不会将一次迭代中的第二个存储与下一次迭代中的第一个存储合并(如果这是在循环中运行)。或者更一般地,确保尽可能晚地完成使区域无效的商店,以减少误报。 (可能没有必要使用真正的编译器,并且在调用此函数之间有大量代码。但是signal_fence从不编译任何指令,并且似乎比将第一个存储保持为mo_release更好的选择。在发布版本的架构上 - 存储线程围栏编译为额外指令,轻松存储避免有两个单独的屏障指令。)

我还担心第一家商店可能会使用上一次迭代中的发布商店进行重新排序。但我不认为这种情况会发生,因为两家商店的地址相同。 (在编译时,也许标准允许恶意编译器执行此操作,但任何理智的编译器都会根本不执行其中一个存储,如果它认为可以传递另一个存储库。)在运行时弱的情况下 - 有序的架构,我不确定同一地址的商店是否可能无序地全局可见。 这在现实生活中不应该是一个问题,因为生产者可能不是背对背的。

BTW,您正在使用的同步技术是Seqlock ,但只有一个单一作家。您只有序列部分,而不是锁定部分来同步单独的编写器。在多写程序版本中,编写者会在读取/写入序列号和数据之前获取锁定。 (而不是将seq no作为函数arg,你从锁中读取它。)

C ++标准 - 讨论文件N4455(关于原子的编译器优化,请参阅Can num++ be atomic for 'int num'?的答案的后半部分)以它为例。

而不是StoreStore围栏,他们使用发布商店来存储编写器中的数据项。 (对于原子数据项,正如我所提到的那样,要求它真的是正确的。)

void writer(T d1, T d2) {
  unsigned seq0 = seq.load(std::memory_order_relaxed);  // note that they read the current value because it's presumably a multiple-writers implementation.
  seq.store(seq0 + 1, std::memory_order_relaxed);
  data1.store(d1, std::memory_order_release);
  data2.store(d2, std::memory_order_release);
  seq.store(seq0 + 2, std::memory_order_release);
}

他们谈论让读者对序列号的第二次加载可能会在以后的操作中重新排序,如果编译器这样做是有利的,并且在阅读器中使用t2 = seq.fetch_add(0, std::memory_order_release)作为获取加载的潜在方式释放语义。对于当前的编译器,我会推荐;您可能会在x86上获得lock ed操作,其中我上面提到的方式没有任何(或任何实际屏障指令,因为只有全屏障seq_cst栅栏需要x86上的指令)。