Multithreading 使用危险指针进行无锁内存回收

Multithreading 使用危险指针进行无锁内存回收,multithreading,algorithm,memory-management,lock-free,Multithreading,Algorithm,Memory Management,Lock Free,是一种在无锁代码中安全回收内存而无需垃圾回收的技术 其思想是,在访问可以同时删除的对象之前,线程将其危险指针设置为指向该对象。想要删除对象的线程将首先检查是否有任何危险指针被设置为指向该对象。如果是这样,删除将被推迟,这样访问线程就不会读取已删除的数据 现在,假设我们的删除线程开始迭代危险指针列表,并在i+1元素处被抢占。现在,另一个线程将危险指针设置为i,指向删除线程当前试图删除的对象。之后,删除线程继续,检查列表的其余部分,并删除对象,即使现在在位置i处有一个指向该对象的危险指针 显然,仅仅

是一种在无锁代码中安全回收内存而无需垃圾回收的技术

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

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

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

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

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

  • 从容器或任何其他公共结构上移除。一般来说,取消发布它
  • 释放内存
  • 因此,通过危险指针的迭代必须在它们之间进行,以防止出现您描述的情况:

    另一个线程将危险指针设置为i,指向删除线程当前试图删除的对象

    因为另一个线程必须无法获取要删除的对象。

    权威答案 本指南对使用危险指针的算法提出了这一重要限制:

    该方法需要无锁算法来保证 线程可以在动态节点可能被删除时访问该节点 除非至少有一个螺纹的相关危险 指针从 保证可以从对象的根访问节点。这个 方法防止连续释放任何失效节点 由来自的一个或多个线程的一个或多个危险指针指向 拆除前的一点

    这对删除线程意味着什么 正如中所指出的,删除是一个分为两个阶段的操作:首先必须“取消发布”节点,将其从数据结构中删除,以便不再可以从公共接口访问它

    在这一点上,节点可能被移除,用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. 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
    之后,那么访问线程访问该节点甚至是安全的(因为有一个危险指针始终指向该节点),但由于访问线程无法区分这种情况,因此它必须错误地失败


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

    看来,至少在检查其他区域的危险指针之前,删除线程必须设置自己的危险指针