繁体   English   中英

C ++如何使无锁堆栈推送原子

[英]c++ how to make lock free stack push atomic

我需要为无锁堆栈编写一个void push(const T& val)实现。 问题是compare_exchange_weak期望使用非原子node*但是我必须使用std::atomic<node*> next字段而不是常规node* next 我试图通过这样做来解决这个问题

void push(const T& val) {
    node* new_node = new node(val);
    node* local_next = new_node->next.load();
    while (!head.compare_exchange_weak(local_next, new_node));
}

但是创建if local_next会使情况变得更糟。 我测试了2种代码变体。 第一个node* next是非原子场node* next ,在下面的测试代码中我丢失了大约20-30个元素。 使用第二个变体,我陷入了僵局。

测试代码:

#include <iostream>
#include <thread>
#include <atomic>
#include "lock_free_stack.h"

using namespace std;

void test(lock_free_stack<int>& st, atomic<int>& sum) {
    st.push(1);
    shared_ptr<int> val(st.pop());
    while (val == nullptr) { }
    sum.store(sum.load() + *val);
}

int main(int argc, const char * argv[]) {
    atomic<int> sum;
    sum.store(0);
    for (int i = 0; i < 100; ++i) {
        lock_free_stack<int> st;
        thread t1(test, ref(st), ref(sum));
        thread t2(test, ref(st), ref(sum));
        thread t3(test, ref(st), ref(sum));
        thread t4(test, ref(st), ref(sum));
        thread t5(test, ref(st), ref(sum));
        thread t6(test, ref(st), ref(sum));
        thread t7(test, ref(st), ref(sum));
        thread t8(test, ref(st), ref(sum));
        t1.join();
        t2.join();
        t3.join();
        t4.join();
        t5.join();
        t6.join();
        t7.join();
        t8.join();
    }
    if (sum.load() == 800) {
        cout << "CORRECT" << endl;
    } else {
        cout << "TIME TO REWRITE STACK " << sum << endl;
    }
    return 0;
}

还有我的无锁堆栈的代码(第一个变体):

#ifndef lock_free_stack_hard_lock_free_stack_h
#define lock_free_stack_hard_lock_free_stack_h

template <typename T>
class lock_free_stack {
private:
    struct node {
        node* next;
        std::shared_ptr<T> value;
        node (const T& val) : value(std::make_shared<T>(val)) { }
    };
    std::atomic<node*> head;
    std::shared_ptr<T> default_value;

public:
    lock_free_stack() : head(nullptr), default_value(std::make_shared<T>()) { }

    void push(const T& val) {
        node* new_node = new node(val);
        new_node->next = head.load();
        while (!head.compare_exchange_weak(new_node->next, new_node));
    }

    std::shared_ptr<T> pop() {
        node* old_head = head.load();
        while (old_head && !head.compare_exchange_weak(old_head, old_head->next));
        if (old_head) {
            return old_head->value;
        } else {
            return std::shared_ptr<T>();
        }
    }
};

#endif

第二个变种:

#ifndef lock_free_stack_hard_lock_free_stack_h
    #define lock_free_stack_hard_lock_free_stack_h

    template <typename T>
    class lock_free_stack {
    private:
        struct node {
            std::atomic<node*> next;
            std::shared_ptr<T> value;
            node (const T& val) : value(std::make_shared<T>(val)) { }
        };
        std::atomic<node*> head;
        std::shared_ptr<T> default_value;

    public:
        lock_free_stack() : head(nullptr), default_value(std::make_shared<T>()) { }

        void push(const T& val) {
            node* new_node = new node(val);
            new_node->next = head.load();
            node* local_next = new_node->next.load();
            while (!head.compare_exchange_weak(local_next, new_node)); 
        }

        std::shared_ptr<T> pop() {
            node* old_head = head.load();
            while (old_head && !head.compare_exchange_weak(old_head, old_head->next));
            if (old_head) {
                return old_head->value;
            } else {
                return std::shared_ptr<T>();
            }
        }
    };

    #endif

因此,最后一个问题是如何正确创建该local_next

谢谢。

测试的一个问题是行sum.store(sum.load() + *val);

使用原子操作,例如sum += *val;

第一种是垃圾,因为您不能保证在存储到node :: next指针的原子性。 可以将内存屏障/屏障与非原子的下一个指针一起使用,但是我不相信这种实现。 您的第二个变体更接近正确的实现。 但是,您忘记了push()CAS循环中最重要的事情:

void push(const T& val) {
    node* new_node = new node(val);
    new_node->next = head.load();
    node* local_next = new_node->next.load();
    while (!head.compare_exchange_weak(local_next, new_node)); 
}

这里的代码分配新节点,加载并存储指向new_node->next head指针。 接下来,代码将已知的堆栈head指针值保存到local_next (不必要的步骤)然后代码尝试将堆栈head更新为new_node而不更新new_node->next 如果您在运行单线程且没有抢占的单线程核心计算机上运行,​​这会很好,并且CAS将成功100%的时间。 ;)

当CAS失败时,它会将head当前新值加载到local_next ,并且代码陷入无限循环,因为new_node永远不会等于local_next 所以你把最后一部分弄错了。

为了使CAS循环正常运行,失败的线程必须重新加载并重新计算它试图更新的数据。 这意味着你必须更新new_node->nexthead重新尝试CAS之前。 这不能解决CAS循环的ABA问题,但我不在回答之列。 我建议阅读更多有关CAS操作及其陷阱的信息。

由于CAS操作做了负载运行的解决方法是很简单,只是存储local_nextnew_node->next失败的CAS后。 这是更有效(未试用)的版本:

node* new_node = new node(val);
node* local_head = head.load();
new_node->next.store(local_head);
while(!head.compare_exchange_weak(local_head, new_node) {
    new_node->next.store(local_head);
}

您将对pop()实现做类似的事情。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM