gpt4 book ai didi

c++ - 使用原子锁定免费的单个生产者多个消费者数据结构

转载 作者:太空狗 更新时间:2023-10-29 21:14:45 24 4
gpt4 key购买 nike

我最近有类似下面的示例代码(实际代码要复杂得多)。看完汉斯·勃姆(Hans Boehm)关于原子的cppcon16讨论后,我有点担心我的代码是否有效。
produce由单个生产者线程调用,而consume由多个使用者线程调用。生产者仅更新序列号为2、4、6、8,...的数据,但在更新数据之前将其设置为奇数序列号(如1、3、5、7 ...),以指示数据可能是脏的。使用者也尝试以相同的顺序(2、4、6,...)获取数据。

消费者在读取后仔细检查序列号以确保数据良好(读取过程中生产者未更新)。

我认为我的代码在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;
}

最佳答案

瞄准x86时,Compile-time reordering仍然会咬你,因为编译器进行了优化以保留程序在C++抽象机上的行为,而不是依赖于体系结构的更强大的行为。由于我们要避免memory_order_seq_cst,因此允许重新排序。

是的,您的商店可以按照您的建议重新订购。从an acquire-load is only a one-way barrier开始,您的加载也可以使用t2加载重新排序。编译器完全优化掉t2检查是合法的。如果可能进行重新排序,则允许编译器确定总是发生这种情况,并应用“视情况”规则来制作更有效的代码。 (当前的编译器通常不这样做,但是当前所写的标准绝对允许这样做。请参见the conclusion of a discussion about this, with links to standards proposals。)

防止重新排序的选项包括:

  • 使所有数据成员使用release发布/获取原子并获取语义。 (最后一个数据成员的获取负载将阻止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似乎是一个更好的选择。在释放存储线程的体系结构上-两者都编译成额外的指令,因此轻松的存储避免了两个单独的屏障指令。)

    我还担心第一家商店与上次迭代中的发行商店重新排序的可能性。但是我认为这不可能发生,因为两家商店都在同一个地址。 (在编译时,也许该标准允许敌对的编译器执行此操作,但是如果任何理智的编译器认为可以通过另一个存储,则根本不执行任何一个存储。)有序体系结构,我不确定存储到同一地址的存储是否会变得全局可见。 在现实生活中这应该不是问题,因为大概不会将生产者称为背靠背。

    顺便说一句, 您正在使用的同步技术是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上使用指令)。

    关于c++ - 使用原子锁定免费的单个生产者多个消费者数据结构,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39956189/

    24 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com