我想知道是否可以为任何“通用”架构(如x64或ARMv7/ARMv8)创建一个无锁、线程安全的共享指针。
在cppcon2014关于无锁编程的演讲中,Herb Sutter展示了一个无锁单链表的(部分)实现。实现看起来非常简单,但是它依赖于标准库中尚不存在的原子< code>shared_ptr实现,或者依赖于使用专用的< code>std::atomic...功能。这一点尤其重要,因为单个push/pop调用可能会调用多个原子加载/存储和< code>compare_exchange操作。
我所看到的问题(我认为演讲中的一些问题也指向同一个方向)是,要使这成为一个真正的无锁数据结构,那些原子操作本身必须是无锁的。我不知道任何< code>std::atomic的标准库实现...函数是无锁的——至少通过简短的google / SO搜索——我也没有找到如何为< code>std::atomic实现无锁专门化的建议
在我浪费时间之前,我想问:
作为参考,下面是来自Herb Sutter的代码(可能包含我的拼写错误):
template<class T>
class slist {
struct Node { T t; std::shared_ptr<Node> next; };
std::atomic<std::shared_ptr<Node>> head;
public:
class reference{
std::shared_ptr<Node> p;
public:
reference(std::shared_ptr<Node> p_){}
T& operator*(){ return p->t; }
T* operator->(){ return &p->t; }
};
auto find(T t) const {
auto p = head.load();
while (p && p-> != t) {
p = p - next;
}
return reference(move(p));
}
void push_front(T t) {
auto p = std::make_shared<Node>();
p->t = t;
p->next = head;
while (!head.compare_exchange_weak(p->next, p)) {}
}
void pop_front() {
auto p = head.load();
while (p && !head.compare_exchange_weak(p, p - next)) { ; }
}
};
请注意,在这个实现中,shared_ptr
的单个实例可以被多个不同的线程访问/修改。它可以被读取/复制、重置甚至删除(作为节点的一部分)。所以这不是关于多个不同的shared_ptr
对象(管理同一个对象)是否可以被多个线程在没有竞争条件的情况下使用——这对于当前的实现来说已经是正确的,也是标准所要求的——而是关于对单个指针实例的并发访问,对于标准的共享指针来说,不会比对原始指针的相同操作更安全。
解释一下我的动机:
这主要是一个学术问题。我无意在生产代码中实现我自己的无锁列表,但我发现这个话题很有趣,乍一看,Herb 的演示文稿似乎是一个很好的介绍。然而,在思考这个问题和@sehe对我的回答的评论时,我想起了这个演讲,又看了一眼,意识到如果 Herb 的实现需要锁(他们目前确实如此),那么将 Herb 的实现称为无锁是没有多大意义的。所以我想知道,这是否只是当前实现的限制或设计中的根本缺陷。
我添加这个作为答案,因为它太长了,无法放入评论中:
需要考虑的事情。实现无锁/无等待数据结构不需要无锁shared_ptr。
Sutter在他的演示中使用shared_ptr的原因是因为编写无锁数据结构最复杂的部分不是同步,而是内存回收:当节点可能被其他线程访问时,我们不能删除它们,所以我们必须泄漏它们并在以后回收。无锁shared_ptr实现本质上提供了“自由”的内存回收,并使无锁代码的例子变得易于接受,尤其是在有时间限制的演示环境中。
当然,将无锁的atomic_shared_ptr作为标准的一部分会有很大的帮助。但这不是为无锁数据结构进行内存回收的唯一方式,还有一个简单的实现,即维护一个在执行中的静态点要删除的节点列表(仅在低争用情况下有效)、危险指针、使用拆分计数的自主原子引用计数。
至于性能,@mksteve是正确的,除非它运行在一个提供真正并发性的高度并行系统上,否则无锁代码不一定会优于基于锁的代码。它的目标是实现最大的并发性,因此,我们通常得到的是线程以执行更多的工作为代价来减少等待。
PS 如果你对此感兴趣,你应该考虑看看 C 并发在行动 由 Anthony Williams.它用一整章的篇幅来编写无锁/无等待代码,这提供了一个很好的起点,介绍了无锁堆栈和队列的实现。
>
你知道,如果有可能写一个无锁的原子共享指针吗?
是否已经有任何我忽略的实现,并且 - 理想情况下 - 甚至与您对 std::atomic 的期望兼容?
我认为标准::atomic_...提供了一种实现形式,其中 slist 将对shared_ptr执行特殊的atomic_查询。将其分为两类(std::atomic 和 std::shared_ptr)的问题在于,它们都有必须遵守的约束才能发挥作用。阶级分离,使得共享约束的知识变得不可能。
在知道这两个项目的slist中,它可以帮助情况,因此可能是atomic_...函数将起作用。
>
<区块引用> 如果没有办法在当前架构上实现它,与受锁保护的“普通”链表相比,您认为Herb的实现还有其他好处吗?
来自维基百科:无阻塞算法是无锁的本质,目的是保证至少一个线程正在取得一些进展。
这并不能保证比锁定的实现有更好的性能,但是可以保证不会发生死锁。
假设<code>T</code>需要一个锁来执行复制,这也可能是列表之外的一些操作所拥有的。如果它被拥有,那么就可能出现死锁,并调用了slist的基于锁的实现。
我认为CAS是在< code > STD::compare _ exchange _ weak 中实现的,所以是独立于实现的。
目前用于复杂结构(例如向量、映射)的无锁算法往往比锁算法效率低得多,Dobbs博士:无锁数据结构但提供的好处(提高线程性能)将显著提高计算机的性能,计算机往往有大量空闲的CPU。
对算法的进一步研究可能会发现可以在未来的CPU中实现的新指令,从而为我们提供无等待的性能和提高的计算资源利用率。
可以编写一个无锁的共享 ptr,因为唯一需要更改的是计数。ptr 本身只是复制的,因此这里不需要特别小心。删除时,这必须是最后一个实例,因此其他线程中不存在其他副本,因此没有人会在同一时间内递增。
但话虽如此,std::atomic.
我见过一些无锁链表的实现,但是没有一个是共享指针的。这些容器通常有特殊的用途,因此它们的使用(何时/谁创建/删除)有一个协议,所以不需要使用共享指针。< br >此外,共享指针引入了一个开销,这与我们的低延迟目标背道而驰,而我们的低延迟目标最初将我们带到了无锁域。< br >
所以,回到你的问题——我认为这是可能的,但我不明白为什么要这样做。
如果你真的需要这样的东西,一个refCount成员变量会更好。
我认为Herb的具体实现没有什么特别的好处,也许除了学术上的好处,但无锁列表显然有没有锁的动机。它们通常充当队列,或者只是在对锁过敏的线程之间共享节点集合<也许我们应该问问赫伯。。药草你在听吗?
编辑:在下面的所有评论之后,我实现了一个无锁的单链表。该列表相当复杂,以防止共享 ptr 在访问时被删除。它太大了,不能在这里发布,但这里是主要思想: -
主要思想是将已删除的条目存放在单独的位置 - 垃圾收集器 - 以使以后的操作无法访问它们。
- 原子引用计数在进入每个功能(push_front、pop_front和前端)时递增,并在退出时自动递减。当递减到零时,版本计数器将递增。多在一个原子指令中。
- 当共享 ptrs 需要擦除时,pop_front,它被推送到 GC 中。每个版本号都有一个 GC。GC 使用更简单的无锁列表实现,该列表只能push_front或pop_all。我已经创建了一个包含 256 个 GC 的循环缓冲区,但可以应用其他一些方案。
- 版本的 GC 在版本增量时刷新,然后共享 ptrs 删除持有者。
因此,如果您调用 pop_front,在没有运行任何其他内容的情况下,引用计数将增加到 1,前面的共享 ptr 被推入 GC[0],引用计数回到零,版本为 1,GC[0] 被刷新 - 它会减少我们弹出的共享 ptr,并可能删除它拥有的对象。
现在,编写一个无锁shared_ptr。我相信这是可行的。以下是我想到的一些想法:
-你可以使用指向持有者指针的低位来设置一个旋转锁,因此只有在锁定后才能取消引用它。你可以使用不同的位来进行inc/dec等操作。这比锁定整个对象要好得多
这里的问题是,共享的ptr本身可以被删除,因此它包含的任何内容都必须提供一些外部保护,如链接列表
-您可以有一些共享指针的中央注册表。这不会受到上述问题的影响,但偶尔在没有延迟峰值的情况下进行扩展是很困难的
总而言之,我目前认为这整个想法是没有实际意义的。如果您找到了其他不会遇到大问题的方法,我会非常好奇地想知道:)< br >谢谢!