簡體   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