带危险指针的无锁内存回收

Posted

技术标签:

【中文标题】带危险指针的无锁内存回收【英文标题】:Lock-free memory reclamation with hazard pointers 【发布时间】:2014-08-08 13:20:04 【问题描述】:

Hazard pointers 是一种无需垃圾收集即可在无锁代码中安全回收内存的技术。

这个想法是,在访问可以同时删除的对象之前,线程将其危险指针设置为指向该对象。想要删除对象的线程将首先检查是否有任何危险指针设置为指向该对象。如果是这样,删除将被推迟,因此访问线程不会最终读取已删除的数据。

现在,假设我们的删除线程开始迭代危险指针列表,并在 i+1 元素处被抢占。现在另一个线程将i 处的危险指针设置为删除线程当前正在尝试删除的对象。之后,删除线程继续,检查列表的其余部分,并删除该对象,即使现在在位置 i 处有一个危险指针指向该对象。

很明显,仅仅设置危险指针是不够的,因为删除线程可能已经检查了我们的危险指针并决定我们的线程不想访问该对象。在设置危险指针后,如何确保我尝试访问的对象不会从我手中删除?

【问题讨论】:

看来,删除线程至少要设置自己的危险指针,然后再检查其他线程的危险指针。 @VaughnCato 这样做你会做什么?设置危险指针是在没有首先检查列表的情况下完成的,因此您无法以这种方式与访问线程同步。您也可以不与删除线程同步以避免双重删除,因为您会遇到问题中描述的完全相同的比赛。 阅读 Maged Michael 的 original paper 中的示例代码可能会有所帮助。 不会有任何其他线程有一个指向被删除对象的指针已经在它的危险列表中吗?我认为在一个对象被淘汰之前,除了要删除它的线程之外,没有任何东西会指向它。 感谢大家的建议。我想我现在明白了。我添加了一个社区 wiki 答案来充实它应该如何工作。我希望你觉得它有用。 【参考方案1】:

权威答案

original paper by Maged M. Michael 对使用危险指针的算法设置了这一重要限制:

该方法需要无锁算法来保证没有 线程可以在动态节点可能被删除时访问它 来自对象,除非至少有一个线程相关的危险 指针一直指向那个节点,从 保证从对象的根可以访问该节点。这 方法防止不断释放任何退休节点 由一个或多个线程的一个或多个危险指针指向 删除之前的一个点。

删除线程的意义

正如Anton's answer 中所指出的,删除是一个两阶段操作:首先,您必须“取消发布”节点,将其从数据结构中删除,这样就不能再从公共接口访问它。

此时,按照迈克尔的说法,该节点可能已被删除。并发线程访问它不再安全(除非它们自始至终都持有指向它的危险指针)。

因此,一旦一个节点可能被删除,删除线程迭代危险指针列表是安全的。即使删除线程被抢占,并发线程也可能不再访问该节点。在验证没有为节点设置危险指针后,删除线程可以安全地进行删除的第二阶段:实际释放。

总的来说,删除线程的操作顺序是

D-1. Remove the node from the data structure.
D-2. Iterate the list of hazard pointers.
D-3. If no hazards were found, delete the node.

真正的算法稍微复杂一些,因为我们需要维护一个无法回收的节点列表,并确保它们最终被删除。此处已跳过,因为它与解释问题中提出的问题无关。

对访问线程的意义

设置危险指针不足以保证安全访问它。毕竟,在我们设置危险指针时,该节点可能已被删除。

确保安全访问的唯一方法是,如果我们可以保证我们的危险指针一直指向该节点,从保证从对象的根可以访问该节点开始

由于代码应该是无锁的,因此只有一种方法可以实现:我们乐观地将危险指针设置为节点,然后检查该节点是否已被标记为可能已删除(即它是不再可以从公共根访问)之后

因此访问线程的操作顺序是

A-1. Obtain a pointer to the node by traversing the data structure.
A-2. Set the hazard pointer to point to the node.
A-3. Check that the node is still part of the data structure.
     That is, it has not been possibly removed in the meantime.
A-4. If the node is still valid, access it.

影响删除线程的潜在种族

在一个节点可能被删除后(D-1),删除线程可能会被抢占。因此,并发线程仍然可以乐观地将其危险指针设置为它(即使不允许它们访问它)(A-2)。

因此,删除线程可能会检测到虚假危险,从而阻止它立即删除节点,即使其他线程将不再访问该节点。这只会以与合法危害相同的方式延迟删除节点。

重点是节点最终还是会被删除。

影响访问线程的潜在竞争

在验证节点未被潜在删除之前,访问线程可能会被删除线程抢占 (A-3)。在这种情况下,它不再被允许访问该对象。

请注意,如果抢占发生在A-2之后,访问线程甚至可以安全访问该节点(因为始终存在指向该节点的危险指针),但是由于无法访问线程来区分这种情况,它必须虚假失败。

重要的一点是节点只有在没有被删除的情况下才会被访问。

【讨论】:

我觉得用“抢占”来谈论多个线程同时做事的情况很奇怪。如果你只有一个单处理器系统,那么抢占是线程异步交错操作的唯一方法,synchronization would be much cheaper。【参考方案2】:

想要删除对象的线程将首先检查是否有任何危险指针设置为指向该对象。

这就是问题所在。 “删除”实际上是两阶段操作:

    从容器或任何其他公共结构中移除。一般来说,取消发布它。 释放内存

因此,通过危险指针的迭代必须在它们之间进行,以防止出现您描述的情况:

另一个线程将 i 处的危险指针设置为删除线程当前正在尝试删除的对象

因为必须没有其他线程可以获取正在删除的对象。

【讨论】:

以上是关于带危险指针的无锁内存回收的主要内容,如果未能解决你的问题,请参考以下文章

Java的内存回收机制

关于仿照java的内存回收机制实现C++的自动内存回收的一点想法

C#内存管理与垃圾回收

C指针原理(28)-垃圾回收-内存泄露

Qt中的内存回收机制

android 垃圾回收机制