如何多线程"尾部呼叫"使用TBB进行递归

时间:2014-05-24 13:31:36

标签: c++ multithreading c++11 recursion tbb

我正在尝试使用tbb来多线程化现有的递归算法。单线程版本使用尾调用递归,从结构上看,它看起来像这样:

void my_func() {
    my_recusive_func (0);
}

bool doSomeWork (int i, int& a, int& b, int& c) {
    // do some work
}

void my_recusive_func (int i) {
    int a, b, c;
    bool notDone = doSomeWork (i, a, b, c);
    if (notDone) {
        my_recusive_func (a);
        my_recusive_func (b);
        my_recusive_func (c);
    }
}

我是tbb新手所以我的第一次尝试使用了parallel_invoke函数:

void my_recusive_func (int i) {
    int a, b, c;
    bool notDone = doSomeWork (i, a, b, c);
    if (notDone) {
        tbb::parallel_invoke (
                [a]{my_recusive_func (a);},
                [b]{my_recusive_func (b);},
                [c]{my_recusive_func (c);});
    }
}

这确实有效,并且运行速度比单线程版本快,但它似乎不能很好地扩展核心数量。我的目标机器有16个内核(32个超线程),因此可伸缩性对于这个项目非常重要,但是这个版本在该机器上最多只能获得8倍的加速,并且在算法运行时许多内核似乎处于空闲状态

我的理论是tbb在parallel_invoke之后等待子任务完成,所以可能有许多任务闲置等待不必要?这会解释空闲核心吗?有没有办法让父任务返回而不等待孩子?我当时想的可能是这样的,但我对调度程序的了解不够,但还不知道这是否合适:

void my_func()
{
    tbb::task_group g;
    my_recusive_func (0, g);
    g.wait();
}

void my_recusive_func (int i, tbb::task_group& g) {
    int a, b, c;
    bool notDone = doSomeWork (i, a, b, c);
    if (notDone) {
        g.run([a,&g]{my_recusive_func(a, g);});
        g.run([b,&g]{my_recusive_func(b, g);});
        my_recusive_func (c, g);
    }
}

我的第一个问题是tbb::task_group::run()线程安全吗?我无法从文档中找到答案。此外,还有更好的方法来解决这个问题吗?也许我应该使用低级调度程序调用?

(我输入的代码没有编译,所以请原谅错别字。)

3 个答案:

答案 0 :(得分:3)

我非常确定tbb::task_group::run()是线程安全的。我无法在文档中找到提及,这是非常令人惊讶的。

然而,

  • This 2008 blog post包含task_group的基本实现,其run()方法明确指出是线程安全的。目前的实施非常相似。
  • tbb::task_group的测试代码(在src/test/test_task_group.cpp中)附带了一个测试,用于测试task_group的线程安全性(它产生了一堆线程,每个线程调用{同一run()个对象上的{1}}千倍或更多。
  • TBB附带的task_group示例代码(在sudoku中)也从递归函数中的多个线程调用examples/task_group/sudoku/sudoku.cpp,基本上与您提出的代码相同。
  • task_group::run是TBB与Microsoft的PPL共享的功能之一,其task_groupthread-safe。虽然TBB文档提醒说TBB和PPL版本之间的行为仍然存在差异,但如果线程安全(因此需要外部同步)这些基本内容不同,那将是非常令人惊讶的。
  • task_group(描述为"与tbb::structured_task_group一样,但只有一部分功能")明确限制"方法task_grouprunrun_and_waitcancel只能由创建wait"。
  • 的主题调用。

答案 1 :(得分:3)

这里确实有两个问题:

  1. task_group :: TBB的TBB实现是否是线程安全的?是。 (我们应该更清楚地记录这一点。)
  2. 是否有许多线程在相同的 task_group可伸缩上调用方法run()?不。(我相信Microsoft文档在某处提到了这一点。)原因是task_group成为一个集中的争用点。它只是实现中的一个获取和添加,但由于受影响的缓存行必须反弹,因此最终仍然无法撤消。
  3. 通常最好从task_group中生成少量任务。如果使用递归并行,请为每个级别提供自己的task_group。虽然性能可能不会比使用parallel_invoke更好。

    低级tbb :: task接口是最好的选择。您甚至可以使用tasK :: execute返回指向尾调用任务的指针来编写尾递归代码。

    但我有点担心空转线程。我想知道是否有足够的工作来保持线程繁忙。考虑先做work-span analysis。如果您使用的是英特尔编译器(或gcc 4.9),您可以先尝试使用Cilk版本。如果这不会加速,那么即使是低级别的tbb :: task接口也不太可能有所帮助,需要检查更高级别的问题(工作和跨度)。

答案 2 :(得分:0)

您也可以按如下方式实现:

constexpr int END = 10;
constexpr int PARALLEL_LIMIT = END - 4;
static void do_work(int i, int j) {
    printf("%d, %d\n", i, j);
}

static void serial_recusive_func(int i, int j) {
    // DO WORK HERE
    // ...
    do_work(i,j);
    if (i < END) {
        serial_recusive_func(i+1, 0);
        serial_recusive_func(i+1, 1);
        serial_recusive_func(i+1, 2);
    }
}

class RecursiveTask : public tbb::task {
    int i;
    int j;
public:
    RecursiveTask(int i, int j) :
        tbb::task(),
        i(i), j(j)
    {}
    task *execute() override {
        //DO WORK HERE
        //...
        do_work(i,j);
        if (i >= END) return nullptr;
        if (i < PARALLEL_LIMIT) {
            auto &c = *new (allocate_continuation()) tbb::empty_task();
            c.set_ref_count(3);
            spawn(*new(c.allocate_child()) RecursiveTask(i+1, 0));
            spawn(*new(c.allocate_child()) RecursiveTask(i+1, 1));
            recycle_as_child_of(c);
            i = i+1; j = 2;
            return this;
        } else {
            serial_recusive_func(i+1, 0);
            serial_recusive_func(i+1, 1);
            serial_recusive_func(i+1, 2);
        }
        return nullptr;
    }
};
static void my_func()
{
    tbb::task::spawn_root_and_wait(
        *new(tbb::task::allocate_root()) RecursiveTask(0, 0));
}
int main() {
    my_func();
}

您的问题没有包含有关“在这里工作”的大量信息,因此我的实现不会给do_work很多机会返回值或影响递归。如果您需要,您应该编辑您的问题,以便提及“在这里工作”会对整体计算产生什么样的影响。