我试图了解无锁编程,并编写了无锁堆栈:
template <typename T>
class LockFreeStack
{
struct Node
{
std::shared_ptr<T> data;
Node* next;
explicit Node(const T& _data)
: data(std::make_shared<T>(_data)), next(nullptr)
{}
};
std::atomic<Node*> head;
public:
void push(const T& data)
{
auto n{new Node(data)};
n->next = head.load();
while (!head.compare_exchange_weak(n->next, n))
;
}
std::shared_ptr<T> pop(void)
{
auto old_head{head.load()};
while (old_head && head.compare_exchange_weak(old_head, old_head->next))
;
return old_head ? old_head->data : std::shared_ptr<T>{};
}
};
和两个用于推/弹出操作的线程:
static LockFreeStack<int> global_stack;
和main
函数:
int main(void)
{
std::srand(std::time(nullptr));
std::thread pushing_thread([](void) {
for (size_t i{}; i < MAX_LENGTH; ++i)
{
const auto v{std::rand() % 10000};
global_stack.push(v);
std::cout << "\e[41mPoping: " << v << "\e[m" << std::endl;
}
});
std::thread poping_thread([](void) {
for (size_t i{}; i < MAX_LENGTH; ++i)
{
if (auto v{global_stack.pop()}; v)
{
std::cout << "\e[42mPushing: " << *v << "\e[m" << std::endl;
}
}
});
pushing_thread.join();
poping_thread.join();
}
此程序仅在调试模式下运行pushing_thread
,但是当我使用调试器运行该程序时,它将按预期运行两个线程,或者如果我在两个线程之间稍等片刻:
std::thread pushing_thread(...);
std::this_thread::sleep_for(1s);
std::thread poping_thread(...);
它正常工作。那么,当我们使用调试器运行程序时会发生什么?
GCC 9.3
。-std=c++2a -lpthread -Wall
。ArchLinux with linux-5.5.13
。答案 0 :(得分:1)
这种逻辑有缺陷:
while (old_head && head.compare_exchange_weak(old_head, old_head->next))
;
如果old_head不为null,并且交换的文件有效,则重试!
答案 1 :(得分:1)
您的实现遇到了所谓的ABA问题。请考虑以下情形:
A->B->C
(即,头指向A)A
的地址)和A
的下一个指针(B
),但是在执行CAS之前,它被中断了。 A
,然后弹出B
,然后再推A
,即堆栈现在看起来像这样:A->C
A
更新为B
->您的堆栈已损坏-看起来像这样:B->C
-但是B
当前在由线程2使用。有几种可能的方法可以避免ABA问题,例如标记指针或并发内存回收方案。
更新:
带标记的指针只是一个用版本计数器扩展的指针,每次更新该指针时,版本标记都会递增。您可以使用DWCAS(Double-Width-CAS)用单独的版本字段更新结构,也可以将版本标签压缩到指针的高位。并非所有的体系结构都提供DWCAS指令(x86可以),并且如果未使用高位,则取决于操作系统(在Windows和Linux上,通常可以使用最高的16位)。
关于内存回收方案的主题,我可以向您介绍我的论文:Effective memory reclamation for lock-free data structures in C++