在C / C ++中实现工作窃取队列?

时间:2010-01-20 13:57:01

标签: c++ multithreading algorithm queue work-stealing

我正在寻找在C / CPP中正确实施工作窃取队列的方法。我环顾了谷歌,但没有找到任何有用的东西。

也许有人熟悉一个好的开源实现? (我不想实施原始学术论文中的伪代码)。

13 个答案:

答案 0 :(得分:13)

看看英特尔的线程构建模块。

http://www.threadingbuildingblocks.org/

答案 1 :(得分:13)

没有免费午餐。

请查看the original work stealing paper。这篇论文很难理解。我知道那篇论文包含理论证明而不是伪代码。但是,没有比TBB更简单的更多简单版本。如果有的话,它将无法提供最佳性能。工作窃取本身会产生一些开销,因此优化和技巧非常重要。特别是,出列必须是线程安全的。实现高度可扩展和低开销的同步具有挑战性。

我真的很想知道你为什么需要它。我认为正确的实现意味着像TBB和Cilk。再次,工作窃取很难实现。

答案 2 :(得分:13)

实施“偷工作”在理论上并不难。您需要一组包含任务的队列,这些任务通过结合计算和生成其他任务来完成更多工作。您需要对队列进行原子访问才能将新生成的任务放入这些队列中。最后,您需要一个每个任务最后调用的过程,以便为执行任务的线程找到更多工作;该程序需要查找工作队列才能找到工作。

大多数此类工作窃取系统假设存在少量线程(通常由实际处理器核心备份),并且每个线程只有一个工作队列。然后你首先尝试从你自己的队列中窃取工作,如果它是空的,试着从别人那里窃取。棘手的是知道要查找哪些队列;为了工作而连续扫描它们非常昂贵,并且可能在寻找工作的线程之间产生大量争用。

到目前为止,这是非常通用的东西,有两个主要的例外:1)切换上下文(例如,设置处理器上下文寄存器,如“堆栈”)不能在纯C或C ++中声明。您可以通过同意在特定于目标平台的机器代码中编写部分包来解决此问题。 2)对于多处理器的队列的原子访问不能完全用C或C ++(忽略Dekker的算法)来完成,因此您需要使用汇编语言同步原语(如X86 LOCK XCH或Compare和Swap)对这些进行编码。现在,一旦你有安全访问权限,更新queuse所涉及的代码并不是很复杂,你可以轻松地在几行C中编写它。

但是,我认为您会发现尝试使用混合汇编程序在C和C ++中编写这样的程序包仍然效率很低,并且最终无论如何最终都会在汇编程序中编写整个程序。剩下的就是C / C ++兼容的入口点: - }

我为我们的PARLANSE并行编程语言做了这个,它提供了在任何时刻实时和交互(同步)的任意大量并行计算的想法。它在X86的幕后实现,每个CPU只有一个线程,并且实现完全在汇编程序中。工作窃取代码总共可能是1000行,而且它的代码很棘手,因为你希望它在非争用情况下非常快。

对于C和C ++,美中不足的是,当你创建一个代表工作的任务时,你分配了多少堆栈空间?串行C / C ++程序通过简单地分配大量(例如,10Mb)一个线性堆栈来避免这个问题,并且没有人关心浪费了多少堆栈空间。但是,如果您可以创建数千个任务并让它们全部在特定时刻生效,那么您无法合理地为每个任务分配10Mb。所以现在你需要静态地确定一个任务需要多少堆栈空间(Turing-hard),或者你需要分配堆栈块(例如,每个函数调用),广泛使用的C / C ++编译器不做(例如,您可能使用的那个)。最后一条出路是限制任务创建,以便在任何时刻将其限制为几百,并在现场任务中复用几百个非常庞大的堆栈。如果任务可以互锁/暂停状态,则无法执行最后操作,因为您将遇到阈值。因此,只有在的任务执行计算时才能执行此操作。这似乎是一个非常严格的限制。

对于PARLANSE,我们构建了一个编译器,为每个函数调用在堆上分配激活记录。

答案 3 :(得分:2)

有一种工具可以非常优雅地完成它。这是在很短的时间内对您的程序进行并行化的一种非常有效的方法。

Cilk project

  

HPC挑战奖

     

我们的高科技挑战赛的Cilk参赛作品   2级奖项获得2006年度奖项   ``优雅与优雅的最佳结合   性能''。该奖项是在   SC'06于2006年11月14日在坦帕举行。

答案 4 :(得分:2)

如果您正在寻找基于pthread或boost :: thread构建的C ++中的独立工作队列类实现,祝您好运,据我所知,没有一个。

然而,正如其他人所说,Cilk,TBB和微软的PPL都在幕后工作。

问题是你想使用一个worktealing队列还是实现一个?如果您只想使用一个,那么上面的选择是很好的起点,只需在其中任何一个中安排“任务”就足够了。

正如BlueRaja所说,task_group& PPL中的structured_task_group也会这样做,同时请注意,最新版本的英特尔TBB中也提供了这些类。并行循环(parallel_for,parallel_for_each)也通过worktealing实现。

如果你必须查看源代码而不是使用实现,TBB就是OpenSource,微软也会为其CRT提供源代码,因此你可以进行探索。

您还可以在Joe Duffy的博客上查看C#实现(但它是C#,内存模型不同)。

-Rick

答案 5 :(得分:1)

structured_task_groupPPL类使用工作窃取队列来实现它。如果您需要WSQ进行线程处理,我建议您这样做 如果您实际上在寻找源代码,我不知道代码是在ppl.h中给出还是有预编译对象;我将在今晚回家时检查。

答案 6 :(得分:1)

我发现这个工作窃取算法的最接近的实现是Karl-FilipFaxén所谓的Woolsrc / report / comparison

答案 7 :(得分:1)

OpenMP可能很好地支持工作窃取,尽管它被称为递归并行

OpenMP forum post

  

OpenMP规范定义了任务构造(可以嵌套,因此非常适合递归并行),但不指定它们如何实现的细节。 OpenMP实现(包括gcc)通常使用某种形式的工作窃取任务,但确切的算法(以及由此产生的性能)可能会有所不同!

请参阅#pragma omp task#pragma omp taskwait

<强>更新

本书C++ Concurrency in Action的第9章描述了如何实现&#34;工作窃取池线程&#34;。我自己还没有阅读/实现它,但它看起来并不太难。

答案 8 :(得分:1)

此开放源代码库https://github.com/cpp-taskflow/cpp-taskflow自2018年12月以来一直支持窃取线程池的工作。

看看function showllum() { $.get('llum.txt', function(data) { $('#showdada').empty(); const src = `${data.trim()}.png`.toLowerCase(); $('#showdada').prepend($('<img>', { id: src.replace(/[ .]/g,''), src }) }); } 类,该类实现了工作窃取队列,如SPAA,2015年论文“动态循环窃取Deque”中所述。

答案 9 :(得分:0)

将工作任务分解为更小的单位会消除首先偷工作的需要吗?

答案 10 :(得分:0)

我已将this C project移植到C ++。

扩展数组时,原始Steal可能会遇到脏读。我试图修复这个bug,但最终放弃了,因为我实际上并不需要一个动态增长的堆栈。 Push方法只返回false,而不是尝试分配空间。然后呼叫者可以执行旋转等待,即while(!stack->Push(value)){}

#pragma once
#include <atomic>

  // A lock-free stack.
  // Push = single producer
  // Pop = single consumer (same thread as push)
  // Steal = multiple consumer

  // All methods, including Push, may fail. Re-issue the request
  // if that occurs (spinwait).

  template<class T, size_t capacity = 131072>
  class WorkStealingStack {

  public:
    inline WorkStealingStack() {
      _top = 1;
      _bottom = 1;
    }

    WorkStealingStack(const WorkStealingStack&) = delete;

    inline ~WorkStealingStack()
    {

    }

    // Single producer
    inline bool Push(const T& item) {
      auto oldtop = _top.load(std::memory_order_relaxed);
      auto oldbottom = _bottom.load(std::memory_order_relaxed);
      auto numtasks = oldbottom - oldtop;

      if (
        oldbottom > oldtop && // size_t is unsigned, validate the result is positive
        numtasks >= capacity - 1) {
        // The caller can decide what to do, they will probably spinwait.
        return false;
      }

      _values[oldbottom % capacity].store(item, std::memory_order_relaxed);
      _bottom.fetch_add(1, std::memory_order_release);
      return true;
    }

    // Single consumer
    inline bool Pop(T& result) {

      size_t oldtop, oldbottom, newtop, newbottom, ot;

      oldbottom = _bottom.fetch_sub(1, std::memory_order_release);
      ot = oldtop = _top.load(std::memory_order_acquire);
      newtop = oldtop + 1;
      newbottom = oldbottom - 1;

      // Bottom has wrapped around.
      if (oldbottom < oldtop) {
        _bottom.store(oldtop, std::memory_order_relaxed);
        return false;
      }

      // The queue is empty.
      if (oldbottom == oldtop) {
        _bottom.fetch_add(1, std::memory_order_release);
        return false;
      }

      // Make sure that we are not contending for the item.
      if (newbottom == oldtop) {
        auto ret = _values[newbottom % capacity].load(std::memory_order_relaxed);
        if (!_top.compare_exchange_strong(oldtop, newtop, std::memory_order_acquire)) {
          _bottom.fetch_add(1, std::memory_order_release);
          return false;
        }
        else {
          result = ret;
          _bottom.store(newtop, std::memory_order_release);
          return true;
        }
      }

      // It's uncontended.
      result = _values[newbottom % capacity].load(std::memory_order_acquire);
      return true;
    }

    // Multiple consumer.
    inline bool Steal(T& result) {
      size_t oldtop, newtop, oldbottom;

      oldtop = _top.load(std::memory_order_acquire);
      oldbottom = _bottom.load(std::memory_order_relaxed);
      newtop = oldtop + 1;

      if (oldbottom <= oldtop)
        return false;

      // Make sure that we are not contending for the item.
      if (!_top.compare_exchange_strong(oldtop, newtop, std::memory_order_acquire)) {
        return false;
      }

      result = _values[oldtop % capacity].load(std::memory_order_relaxed);
      return true;
    }

  private:

    // Circular array
    std::atomic<T> _values[capacity];
    std::atomic<size_t> _top; // queue
    std::atomic<size_t> _bottom; // stack
  };

Full Gist (including unit tests).我只在强大的体系结构(x86 / 64)上运行测试,因此就弱体系结构而言,如果你试图使用它,你的里程可能会有所不同。氖/ PPC。

答案 11 :(得分:-1)

我不认为JobSwarm使用偷工作,但这是第一步。我不知道其他用于此目的的开源库。

答案 12 :(得分:-1)

不知道这对你有什么帮助,但看看关于AMD开发者网络的this文章,它很简单,但应该给你一些有用的东西