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

时间:2015-05-13 19:39:59

标签: c++ multithreading locking free 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));
}

但是如果local_next使事情变得更糟,那就创造出来。我测试了2种代码变体。第一个有非原子场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

谢谢。

2 个答案:

答案 0 :(得分:1)

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

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

答案 1 :(得分:1)

第一个变体是垃圾,因为你不能保证在商店中对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)); 
}

此处代码分配新节点,加载并存储head指向new_node->next的指针。接下来,代码将已知的堆栈head指针值保存到local_next。 (不必要的步骤)然后代码尝试将堆栈head更新为new_node而不更新new_node->next。如果没有先发制人的话,你可以选择运行单线程的核心机器,而CAS将在100%的时间内获得成功。 ;)

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

要创建功能性CAS循环,失败的线程必须重新加载并重新计算它尝试更新的数据。这意味着您必须在重新尝试CAS之前从new_node->next更新head。 这并不能解决CAS循环的ABA问题,但我从答案中删除了它。我建议阅读更多有关CAS操作及其缺陷的内容。

由于CAS操作执行加载操作,修复非常简单,只需在CAS失败后将local_next存储到new_node->next。 这是更有效(未经测试)的版本:

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()实现做类似的事情。