本文中无锁栈的实现,基于对《c++并发编程实践》的学习,对其他无锁数据结构的实现也有借鉴意义。
先看无锁栈的最初版本:
template <typename T>
class lockfreestack
{
struct node
{
std::unique_ptr<T> data;
node *next;
};
std::atomic<node*> head{nullptr};
public:
void push(T data)
{
node *new_node = new node;
new_node->data = new T(std::move(data));
new_node->next = head.load();
while(!head.compare_exchange_weak(new_node->next, new_node));
}
std::unique_ptr<T> pop()
{
node *old_head = head.load();
while(old_head && !head.compare_exchange_weak(old_head, old_head->next)); // I
return old_head ? std::move(old_head->data) : nullptr;
}
};
还不错,除了有内存泄露。在pop中,compare_exchange_weak成功的线程是不能直接delete old_head;
的,因为其他线程可能拿着同样的指针,在取old_head->next。
很自然的想法,要是现在不能释放,那就先收集起来,等能释放的时候释放。
可能有问题的地方在于I的old_head->next,可以维护一个原子变量计数器,进入I之前加1,离开I后减1,当计数器值不为0时,就把要删除的指针存起来,当计数器为0时,就可以把当前的old_head以及之前暂存的指针一起删除掉了。实现细节可参考 chest 中的 lockfreestack_origin.h文件。
上面的计数方法,粒度有点大,处于I处的线程,可能拿到的不是同样的指针,但会导致所有的待删除指针都无法释放。
这里介绍一种风险指针的方法。考虑到每个线程最多只能占用着一个old_head,可以记下每个线程占用着的指针,当想要释放一个指针时,看一下是否有其他线程在占用这个指针,如果没有的话,就可以释放掉。可参考 chest 中的 lockfreestack2.h文件。
第二种方法粒度细一些,可以及时的释放掉内存,但增加了一些计算,可能会得不偿失。
另外,处理失效指针的方法还有epoch-based reclamation,interval-based reclamation等。