【问题标题】:Real-world example where std::atomic::compare_exchange used with two memory_order parameters实际示例,其中 std::atomic::compare_exchange 与两个 memory_order 参数一起使用
【发布时间】:2017-08-19 14:48:55
【问题描述】:

您能否举一个真实的例子,其中使用两个 memory_order 参数版本的std::atomic::compare_exchange 是有原因的(所以一个 memory_order 参数版本不够用)?

【问题讨论】:

    标签: c++ multithreading atomic


    【解决方案1】:

    在许多情况下,compare_exchange 上的第二个内存排序参数设置为 memory_order_relaxed。在这些情况下,省略它通常并没有错,只是效率可能会降低。

    这是一个简单的无锁列表/堆栈示例,它需要在compare_exchange_weak 上使用第二个不同的排序参数才能实现无数据争用。

    push的调用可以并发执行,但是为了避免无锁数据操作的复杂性, 假设在执行对push 的调用时无法从堆栈中删除节点;即避免悬空指针。

    template<typename T>
    class mystack {
    
        struct node {
            node *next = nullptr;
    
            T data;
            int id;
    
            node(int id) : id{id} { }
        };
    
        std::atomic<node *> head{nullptr};
    
    public:
        void push(T data, int id);
        bool pop(T &data); // not implemented
    };
    
    
    template<typename T>
    void mystack<T>::push(T data, int id)
    {
        node *newnode = new node{id};
    
        newnode->data = std::move(data);
    
        node *current_head = head.load(std::memory_order_relaxed);   // A
    
        for (;;)
        {
            newnode->next = current_head;
    
            if (head.compare_exchange_weak(current_head, newnode,
                                           std::memory_order_release,   // B
                                           std::memory_order_acquire))  // C
            {
                /*
                 * 'current_head' may not be derefenced here since the initial load (at A)
                 * does not order memory 'current_head' is pointing at.
                 *
                 * a release barrier (at B) is necessary to make 'newnode' available
                 * to other threads
                 */
                std::cout << "Insertion successful\n";
    
                break;
    
            } else
            {
                /*
                 * 'current_head' is the updated head pointer after 'compare_exchange' failed
                 * Since it was inserted by another thread (the CAS failed),
                 * an acquire barrier must be set (at C) in order to be able to access data
                 * 'current_head' is pointing at.
                 */
                std::cout << "Insertion failed after head changed to id: " <<
                              current_head->id << std::endl;
            }
        }
    }
    

    push 中,初始load(在A 处)是一个宽松的操作,这意味着即使head 指针是原子加载的, 它可能不会被取消引用,因为它引用的内存在此线程中是无序的。

    如果compare_exchange_weak 返回成功,newnode 被插入到列表的头部,并通过设置释放屏障(在 B 处)提供给其他线程。 访问此数据的另一个线程(稍后,通过pop)需要设置获取屏障。

    如果compare_exchange_weak 返回失败(假装忘记),另一个线程刚刚插入了一个新的node 实例并且current_head 被更新为head 的新值。 由于 current_head 现在指向在另一个线程中分配和释放的数据,如果要取消引用 current_head,则需要获取屏障。
    这是真的,因为cout 失败消息包括current_head-&gt;id

    如果省略最后一个参数,第一个屏障参数将用于失败load 场景,但由于这是一个释放屏障, 有效屏障会衰减到memory_order_relaxed,从而导致current_head-&gt;id 上的数据竞争。

    【讨论】:

    • 感谢您提供非常好的答案!只是一个问题:您可以在示例中使用消耗顺序来表示失败吗?我认为答案是肯定的,因为 current_head->id 是依赖读取。
    • @geza 谢谢! .. 是的,在这种情况下,您可以使用 memory_order_consume,因为 (1) current_head 的存储是 compare_exchange 失败负载 current_head 和 (2) @ 987654348@ 带有对current_head-&gt;id. 的依赖项。但不建议这样做; C++17 不赞成使用memory_order_consume,因为“语义被发现不足
    • @LWimsey 编译器作者发现很难说服编译器的中间通道和后端在所有情况下都遵守使用语义,即使在退化的依赖关系中也是如此。此外,后端无法告诉前端将删除依赖项并且可能需要围栏。
    • @curiousguy 是的,我认为这就是不鼓励使用 consume 的原因
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-12-30
    • 1970-01-01
    • 1970-01-01
    • 2021-07-06
    • 2012-03-22
    • 2014-02-28
    • 1970-01-01
    相关资源
    最近更新 更多