【发布时间】:2015-01-01 01:24:19
【问题描述】:
我已经看到了几个过于复杂(在我看来很明显)在 C++ 中实现无锁堆栈(使用像 here 这样的标签),我想出了一个我认为是简单且仍然有效的实现。由于我在任何地方都找不到这个实现(我已经看到 Push 函数的实现类似于我所做的但不是 Pop),我猜测它在某种程度上是不正确的(很可能在 ABA 案例中失败):
template<typename Data>
struct Element
{
Data mData;
Element<Data>* mNext;
};
template<typename Data>
class Stack
{
public:
using Obj = Element<Data>;
std::atomic<Obj*> mHead;
void Push(Obj *newObj)
{
newObj->mNext = mHead.load();
//Should I be using std::memory_order_acq_rel below??
while(!mHead.compare_exchange_weak(newObj->mNext, newObj));
}
Obj* Pop()
{
Obj* old_head = mHead.load();
while (1)
{
if (old_head == nullptr)
return nullptr;
//Should I be using std::memory_order_acq_rel below??
if(mHead.compare_exchange_weak(old_head, old_head->mNext)) ///<<< CL1
return old_head;
}
}
};
我假设 Push 和 Pop 的调用者将负责内存分配和释放。另一种选择是使上述 Push 和 Pop 私有方法,并具有新的公共函数来处理内存分配并在内部调用这些函数。我相信这个实现中最棘手的部分是我用“CL1”标记的那一行。我认为它是正确的并且在 ABA 案例中仍然有效的原因如下:
假设 ABA 案例确实发生了。这意味着“CL1”处的 mHead 将等于 old_head 但他们都指向的对象实际上与我将 mHead 分配给它时最初指向的那个 old_head 不同。但是,我认为即使它是一个不同的对象,我们仍然可以,因为我们知道它是一个有效的“头”。 old_head 指向与 mHead 相同的对象,因此它是堆栈的有效头,这意味着 old_head->mNext 是有效的下一个头。 所以,更新 mHead 为 old_head->mNext 还是正确的!
总结一下:
- 如果 mHead != old_head(另一个线程抢占我们并更改了 mHead)-> old_head 被更新为新的 mHead,我们再次开始循环。
- [NON-ABA] 如果 mHead == old_head -> 简单情况,将 mHead 更新为 old_head->next (==mHead->mNext) 并返回 old_head。
- [ABA] 如果 mHead == old_head -> 按上述说明工作。
那么,我的实现是否有效?我错过了什么?
【问题讨论】:
-
您缺少一个测试工具,其中有几十个线程不断推送和弹出(以及某种检测错误的方法)。
-
@DietmarKühl
compare_exchange_weak在比较失败时将其第一个参数的值更新为观察值,因此循环newObj->mNext的每次行程都设置为mHead的值。首先将其设置为mHead.load()是一个非常小的优化,nullptr将同样正确,但即使没有争用,也需要额外的循环。 -
@Casey:感谢您指出这一点!我可能最好不要评论无锁代码,因为我没有实际经验(即,我删除了我之前的评论......)。
-
这不应该去代码审查吗?
标签: c++ multithreading c++11 stack lock-free