【问题标题】:Multithreaded linked list traversal多线程链表遍历
【发布时间】:2012-09-17 10:42:13
【问题描述】:

给定一个(双)对象链表(C++),我有一个操作,我希望多线程在每个对象上执行。每个对象的操作成本并不统一。出于多种原因,链表是这组对象的首选存储。每个对象中的第一个元素是指向下一个对象的指针;第二个元素是列表中的前一个对象。

我通过构建节点数组并应用 OpenMP 解决了这个问题。这给出了不错的表现。然后我切换到我自己的线程例程(基于 Windows 原语)并通过使用 InterlockedIncrement()(作用于数组的索引),我可以获得更高的整体 CPU 利用率和更快的吞吐量。本质上,线程通过沿元素“跳跃”工作。

我的下一个优化方法是尽量避免在我的链表中创建/重用元素数组。但是,我想继续使用这种“跳跃式”方法,并以某种方式使用一些不存在的例程,可以称为“InterlockedCompareDereference” - 以原子方式与 NULL(列表末尾)进行比较并有条件地取消引用并存储,返回取消引用的值.

我不认为 InterlockedCompareExchangePointer() 会起作用,因为我不能原子地取消引用指针并调用这个 Interlocked() 方法。我已经阅读了一些内容,而其他人则建议使用关键部分或自旋锁。关键部分在这里似乎很重要。我很想尝试自旋锁,但我想我首先在这里提出问题并询问其他人在做什么。我不相信 InterlockedCompareExchangePointer() 方法本身可以像自旋锁一样使用。然后还必须考虑获取/释放/栅栏语义...

想法?谢谢!

【问题讨论】:

    标签: c++ list linked-list


    【解决方案1】:

    关键部分并不是很重。假设他们可以快速获得锁,他们的行为就像一个自旋锁。

    您的问题的解决方案将取决于您是否正在修改列表。如果您不修改列表,您需要做的就是对初始化为 0 的对象中的值进行 InterlockedCompareExchange 之类的操作。交换值为 1,如果返回 0,则执行操作,如果返回 1,你跳过。下次您在列表上执行操作时,您交换 2 并检查 1/2 而不是 0/1。

    如果您要更改列表,那一切都取决于。如果您只想向前移动,并且只删除当前节点,那么您最好的选择是在执行比较交换位、跳跃(获取它的下一个节点)和删除时锁定您锁定的项目中的下一个锁定节点。

    【讨论】:

    • 这适用于少量线程,但是如果您考虑 1000 个线程,那么您最终会遇到 很多 的内存屏障,因为“skipjumping”1000条目很昂贵(10000 个列表项会提供略低于 10M 的内存屏障)
    • 我不是在修改列表,只是想让很多线程高效地遍历列表。
    【解决方案2】:

    我认为 Casta 有一些优点,我会自己弄一个。这个问题的解决方案将非常重视在每个节点上完成的理念转换,它们是否相互依赖,以及完成的任务是否可以在列表的单一扫描中完成。

    如果操作不是相互依赖的,并且扫描方法是有意义的,我建议使用代理获取系统。它实际上与工作组方法相同。每个线程都有一个对管理列表的代理的引用,因为每个线程都需要处理内容,所以它从代理请求它,代理锁定,获取下一个节点,推进内部扫描迭代器,并释放锁定,返回请求线程的节点。这一直持续到列表枚举完成。对于每个节点可能被不同线程多次访问的累加器问题,您可以使用循环列表或其他一些此类容器。有才华的经纪人可以管理列表,包括新的插入、删除等,其方式与分派相同:锁定、操作、解锁。显然有许多活动可以根据您在经纪人方面的特定需求进行定制。这些需求可以构建一个复杂的池管理系统,同时在锁定争用方面仍然非常有效(即几乎没有)。

    然而,底线是显而易见的。了解您的问题集以及您希望每个线程使用其当前节点完成的具体内容。

    【讨论】:

      【解决方案3】:

      基本上,要尽可能快地浏览列表,您需要避免一些事情;

      • 锁定冲突。即使使用自旋锁,每次循环迭代都会错失完成工作的机会。
      • 记忆障碍。每次执行原子操作时,内存屏障都会停止执行。
      • 不必要的工作,例如扫描待办事项列表。

      我必须同意你的阅读并支持自旋锁。

      您将指向列表头部的指针放在一个易失性指针中。

      然后每个线程依次;

      • 获取自旋锁
      • 临时保存指针的值
      • 更新指向下一个列表条目的指针
      • 释放自旋锁。

      然后它可以开始处理临时指向的条目。

      这对于使用每个条目锁定搜索列表有一些优势;

      • 如果每个项目的工作需要很长时间才能完成,那么在更新指针的短时间内您将很少发生冲突。
      • 除非发生冲突,否则每个列表项只有 2 个内存屏障,一个用于锁定,一个用于解锁。
      • 无需扫描列表即可获取新工作项,只需从“队列”中获取工作即可。

      【讨论】:

      • 当使用CRITICAL_SECTION 时,您不需要volatile。锁定关键的调用既是编译器也是 CPU 屏障。
      【解决方案4】:

      由于对齐,列表指针的低位包含零。您可以通过使用比较和交换指令将对象标记为正在处理,以原子方式在其中一个指针中设置这些位之一来利用这一点。

      每个线程都会这样做:

      1. 从列表开始遍历列表。
      2. 对于每个列表节点,检查其下一个指针是否设置了低位。
      3. 如果该位设置为 7。
      4. 比较并交换列表节点的下一个指针与(next | 1)
      5. 如果比较和交换失败转到 7。
      6. 处理对象。
      7. 移动到下一个节点并转到 2。

      另一种选择是将此位标记放入一个在对象本身中具有未使用位的整数中,除非列表节点是基类或对象的成员。

      这样,线程可以从列表中获取对象,而不会阻塞或忙于在wait-free fashion 中旋转。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-09-03
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多