无锁栈可以作为一个单向链表实现。这似乎很简单,直到我们不得不考虑节点在被弹出后该如何处理。一种策略是将它们简单地移动到每个栈的LIFO自由列表(从中节点可以被后续推送操作重用),直到所有线程都完成对栈的操作,此时单个线程销毁栈中的所有节点和自由列表中的所有节点。 Boost.Lockfree 使用这种策略。Chris Wellons's C11 implementation 也是如此。我将提到后者,因为它更容易阅读,而且详细信息基本相同,因为C11原子与C++11原子非常相似。
在Wellons的实现中,可以在这里找到所有lstack_node
对象都是非原子性的。特别地,这意味着对lstack_node
对象的next
成员的所有访问都是非原子性的。我无法理解的是:为什么这样的访问永远不会互相竞争?
next
成员在lstack.c:30处被读取。它在lstack.c:39处被写入。如果这两行代码可以在同一个lstack_node
对象上并发执行,则程序包含一次竞争。这可能吗?对我来说似乎是可能的:
- 线程1调用
lstack_pop
,它调用pop
。它原子性地将头节点的值加载到本地变量orig
中。现在,orig.node
是指向刚刚在堆栈顶部的节点的指针。(注意,直到此时,只有本地变量已被修改,因此在任何其他线程中,线程1迄今所做的任何事情都不可能使CAS失败。) 同时... - 线程2调用
lstack_pop
。pop
成功并返回node
,指向刚从堆栈中移除的节点的指针;这与线程1中orig.node
指向的节点相同。然后,它开始调用push
以将node
添加到自由列表中。自由列表头节点被原子性地加载,并且node->next
被设置为指向自由列表中的第一个节点。 - 糟糕了。这与线程1中对
orig.node->next
的读取竞争。
Wellons的实现可能只是错误的吗?我怀疑。如果他的实现是错误的,那么Boost也是如此,因为修复(在我看来)竞争条件的唯一方法就是使next
成为原子的。但我不认为Boost的实现会在这样一个基本的方面出错,否则它早就被注意到和修复了。所以我在我的推理中肯定犯了错误。