我想知道是否可以为任何“常见”架构(如x64或ARMv7 / ARMv8)创建无锁,线程安全的共享指针。
在关于lock-free programming at cppcon2014的讨论中,Herb Sutter提出了一个(部分)无锁单链表的实现。实现看起来很简单,但它依赖于标准库中不存在或使用专用shared_ptr
函数的原子std::atomic...
实现。这一点尤为重要,因为单个push / pop调用可能会调用多个原子加载/存储和compare_exchange
操作。
我看到的问题(我认为谈话中的一些问题是朝着相同的方向)是因为这是一个实际的无锁数据结构,那些原子操作本身就必须是无锁的。我不知道任何标准库实现的std::atomic...
函数是无锁的 - 至少有一个简短的google / SO搜索 - 我也没有找到如何实现锁定的建议 - std::atomic<std::shared_ptr>
的免费专业化。
在我浪费时间之前,我想问:
std::atomic<std::shared_ptr>
所期望的实现兼容?对于上面提到的队列,它尤其需要CAS
- 操作。供参考,以下是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的演示似乎是一个很好的介绍。然而,在考虑this question和@ sehe对我的回答的评论时,我记得这个话题,再看看它,并意识到如果它是原始操作,调用Herb的实现无锁是没有多大意义的需要锁(他们目前正在做)。所以我想知道,这只是当前实现的限制还是设计中的一个根本缺陷。
答案 0 :(得分:4)
我添加此作为答案,因为它太长而无法发表评论:
要考虑的事情。实现无锁/无等待数据结构需要无锁的shared_ptr 。
Sutter在他的演示文稿中使用shared_ptr的原因是因为编写无锁数据结构最复杂的部分不是同步,而是内存回收:我们无法删除其他线程可能访问的节点,所以我们必须泄漏它们并在以后回收。无锁的shared_ptr实现基本上提供了#34; free&#34;内存回收并使无锁代码的示例变得可口,尤其是在有时间限制的表示的上下文中。
当然,将无锁的atomic_shared_ptr作为标准的一部分将是一个巨大的帮助。但它不是对无锁数据结构进行内存回收的唯一方法,在执行中的静态点维护要删除的节点列表的天真实现(适用于低争用场景)只有),危险指针,使用拆分计数滚动你自己的原子引用计数。
至于性能,@ mksteve是正确的,无锁的代码不能保证优于基于锁的替代方案,除非它可以在提供真正并发性的高度并行系统上运行。它的目标是实现最大的并发性,因此我们通常得到的是线程在执行更多工作的同时不那么等待。
PS如果这是你感兴趣的东西,你应该考虑看一下Anthony Williams的C ++ Concurrency in Action。它专门写了一整章来编写无锁/无等待代码,它提供了一个很好的起点,遍历无锁堆栈和队列的实现。
答案 1 :(得分:1)
你知道吗,如果有可能写一个无锁,原子共享 指针呢?
我是否已经有任何实施 被忽视的 - 理想情况 - 甚至与你想要的东西兼容 期待来自std :: atomic?
我认为std::atomic_...提供了一种实现形式,其中slist将对shared_ptr执行特殊的atomic_查询。将它分成两个类(std :: atomic和std :: shared_ptr)的问题在于它们每个都有需要遵守的约束才能运行。阶级分离使得共享圣徒的知识变得不可能。
在知道两个项目的slist中,它可以帮助解决问题,因此原子_...函数可能会起作用。
如果没有办法在当前体系结构上实现这一点,那么你呢? 看到Herb实施中的任何其他好处与“正常”相比 受锁保护的链表?
从Wikipedia : Non blocking algorithm锁定自由的目的,是为了保证至少一个线程正在取得一些进展。
这并不能保证比锁定的实现更好的性能,但确实可以保证不会发生死锁。
想象一下T
需要一个锁来执行一个副本,这也可能是列表之外的一些操作所拥有的。然后就可能出现死锁,如果它已经拥有,则会调用基于锁的slist实现。
我认为CAS是在std::compare_exchange_weak
中实现的,因此将独立于实现。
复杂结构的当前无锁算法(例如矢量,映射)往往比锁定算法Dr Dobbs : lock-free data structures效率低得多,但提供的好处(改进的线程性能)将显着提高计算机的性能,有大量的空闲cpus。
对算法的进一步研究可以识别可以在未来的CPU中实现的新指令,以便为我们提供无等待的性能并提高计算资源的利用率。
答案 2 :(得分:-1)
可以编写一个无锁共享ptr,因为唯一需要更改的是计数。 ptr本身只是复制,所以这里不需要特别小心。删除时,这必须是最后一个实例,因此其他线程中不存在其他副本,因此没有人会在同一时间内增加
但话说回来,std :: atomic&gt;因为它不是一个原始类型,所以它是一个非常专业的东西。
我已经看到了一些无锁列表的实现,但没有一个是共享指针。这些容器通常具有特殊用途,因此就它们的使用(何时/谁创建/删除)达成一致,因此不需要使用共享指针。
此外,共享指针引入的开销与我们的低延迟目标相反,这些目标首先使我们进入无锁域。
所以,回到你的问题 - 我认为这是可能的,但我不明白为什么这样做。
如果你真的需要这样的东西, refCount 成员变量会更好。(
我认为Herb的具体实施没有特别的好处,可能除了学术上的一个,但无锁列表有明显的动机,没有锁定。它们通常用作队列或仅用于在对锁过敏的线程之间共享节点集合 也许我们应该问Herb .. Herb?你在听吗?
修改强>
根据以下所有评论,我已经实现了一个无锁的单链表。该列表相当复杂,以防止共享ptrs在访问时被删除。这里发布的内容太大了,但这里有一些主要的想法:
- 主要思想是将已删除的条目存储在一个单独的位置 - 垃圾收集器 - 以使后续操作无法访问它们。
- 进入每个函数( push_front , pop_front 和前)时,原子引用计数会递增,并在退出时自动递减。在递减到零时,版本计数器递增。一体化的原子指令
- 当需要删除共享的ptrs时,在 pop_front 中,它会被推送到GC中。每个版本号都有一个GC。 GC使用更简单的无锁列表实现,该列表只能 push_front 或 pop_all 。我已经创建了256个GC的循环缓冲区,但是可以应用其他一些方案
- 版本的GC在版本增量上刷新,然后共享ptrs删除持有者
所以,如果你调用pop_front,没有其他任何东西在运行,则ref计数增加到1,前面的共享ptr被推送到GC [0],ref计数回零,版本为1,GC [0]被刷新 - 它减少我们弹出的共享ptr,并可能删除它拥有的对象。
现在,wrt是一个无锁的shared_ptr。我相信这是可行的。以下是我想到的想法:
- 您可以使用指针的低位来进行各种旋转锁定,因此只有在您锁定它之后才能取消引用它。你可以为inc / dec等使用不同的位。这比锁定整个东西要好得多
这里的问题是共享ptr本身可以被删除,因此包含它的任何东西都必须提供一些外部保护,比如链表。
- 您可以拥有一些共享指针的中央注册表。这不会受到上述问题的影响,但如果在没有延迟峰值的情况下进行扩展将会很难实现。
总而言之,我目前认为这个想法没有实际意义。如果你发现其他方法没有遇到大问题 - 我会非常好奇地了解它:) 谢谢!