危险指针是一种在无锁代码中安全回收内存而不进行垃圾收集的技术。
这个想法是,在访问一个可以并发删除的对象之前,线程设置它的危险指针指向该对象。想要删除对象的线程将首先检查是否设置了指向该对象的危险指针。如果是,删除将被延迟,这样访问线程就不会最终读取被删除的数据。现在,假设我们的删除线程开始迭代危险指针列表,并且在i+1
元素处被抢占。现在,另一个线程将i
的危险指针设置为删除线程当前试图删除的对象。之后,删除线程继续,检查列表的其余部分,并删除对象,即使现在有一个危险指针在位置i
指向该对象。
显然,仅仅设置危险指针是不够的,因为删除线程可能已经检查了危险指针,并决定线程不想访问该对象。我怎么能确保,设置一个危险指针后,我试图访问的对象不会从我的手被删除?
权威答案
Maged M. Michael的原始论文对使用危险指针的算法进行了重要的限制:
该方法需要无锁算法来保证没有线程可以在动态节点可能被删除时访问该节点对象,除非至少有一个线程的相关危险指针一直指向那个节点,从保证从对象的根节点可以访问该节点。的方法防止连续释放任何退役节点中的一个或多个线程的一个或多个危险指针指向移除前的一个点。
删除线程
正如Anton的回答所指出的,删除是一个两阶段的操作:首先你必须"取消发布"节点,将其从数据结构中删除,使其不能再从公共接口访问。
此时,节点可能被移除了,用Michael的话来说。并发线程访问它不再安全(除非它们一直持有一个指向它的危险指针)。因此,一旦节点可能被删除,删除线程就可以安全地迭代危险指针列表。即使删除线程被抢占,并发线程也可能不再访问该节点。在确认没有给节点设置危险指针之后,删除线程可以安全地进入删除的第二阶段:实际的释放。
总之,删除线程的操作顺序是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
之后,访问线程访问节点甚至是安全的(因为在整个过程中有一个危险指针指向该节点),但由于访问线程不可能区分这种情况,因此它必须虚假失败。
要删除对象的线程将首先检查是否设置了指向该对象的危险指针。
问题在这里。'delete'实际上是两个阶段的操作:
- 从容器或任何其他公共结构中移除。一般来说,取消发布。
- 释放内存
因此,通过危险指针的迭代必须在它们之间进行,以防止您描述的情况:
另一个线程将危险指针I设置为删除线程当前试图删除的对象