我需要为无锁堆栈编写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
?
谢谢。
答案 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()实现做类似的事情。