Intel Threading Building Blocks 基于任务编程

为程序争取更多性能时,使用基于线程的方法来编写多线程程序并不是一个好的办法,而更好的是用逻辑任务来表达你的程序

 

关于TBB的安装配置请参考:并行循环(http://www.cppprog.com/2009/0325/92.html)
为程序争取更多性能时,使用基于线程的方法来编写多线程程序并不是一个好的办法,而更好的是用逻辑任务来表达你的程序,理由如下:

  • 与可用资源相匹配的并行化
    你使用线程库所建立的线程是逻辑线程,它们要映射到硬件的物理线程中去。当每个物理线程各自运行一个逻辑线程时效率最高,其它情况下会由于不匹配而致使性能下降。TBB调度试图避免这种不匹配,使一个物理线程对应一个逻辑线程。
  • 任务的启动和停止过程更加迅速
    任务和线程相比关键优势是它们更轻量,在Linux系统,启动和终止一个任务的速度是线程的18倍,在Windows系统,这个比率更是超过了100。这是因为每个线程有自己的一堆资源,象寄存器和堆栈,在Linux里,线程甚至有自己的进程ID。与之形成对比的是任务,它只是一个小例程,而且,任务不是抢先式的。
  • 更高效率的优先级评定
    TBB里的任务效率高在于它们的调度是“不公平的”,线程调度典型的做法是分发时间片,这种分发是“公平的”,因为这是一个在不知道程序的高级别组织形式下最安全的策略。基于任务编程时,任务调度有高级别信息,所以可以为了效率而牺牲公平性。实际上,它经常延迟启动一个任务直到进程确实要用到它为止。
  • 提高负载平衡
    任务调度尽量做到负载平衡,只要把你的程序分解成一组足够小的任务,任务调度通常会很好地分配这些任务到线程中去并让各线程平衡负荷。
  • 处于更高级别思考问题
    最后,一个主要优势是使你处于更高级别的、基于任务来思考。

在并行循环(http://www.cppprog.com/2009/0325/92.html) 一文中,所有例程都首先创建一个task_scheduler_init对象,它就是TBB的任务调度服务。在使用任务之前,也要先创建这个对象。

任务task

    所有的TBB任务都从task继承,并重载其中的纯虚函数task* execute()。

    TBB库还有一个特殊的task,空任务:
  1. // Task that does nothing. Useful for synchronization.
  2. class empty_task: public task {
  3.     /*override*/ task* execute() {
  4.         return NULL;
  5.     }
  6. };

    下面是一个最简单的使用task的例子:

  1. #include <iostream>
  2. #include <tbb/task_scheduler_init.h>
  3. #include <tbb/task.h>
  4.  
  5. using namespace std;
  6. using namespace tbb;
  7.  
  8. struct printtask
  9.     :task
  10. {
  11.     printtask(int n)
  12.         :m_n(n){}
  13.     task* execute()
  14.     {
  15.         cout << m_n;
  16.         return NULL;
  17.     }
  18. private:
  19.     int m_n;
  20. };
  21.  
  22. int main()
  23. {
  24.     task_scheduler_init init;
  25.  
  26.     task *dummy = new(task::allocate_root()) empty_task;
  27.     dummy->set_ref_count(10+1);
  28.     for(int i=0; i<10; i++)
  29.     {
  30.         task* childtask = new(dummy->allocate_child()) printtask(i);
  31.         dummy->spawn(*childtask);
  32.     }
  33.     dummy->wait_for_all();
  34.     dummy->destroy(*dummy);
  35.  
  36.     return 0;
  37. }

    本例中,打印10个数字,分别从0-9。因为任务是并行执行的,所以打印结果是乱序的。

    printtask是一个打印任务,打印一个数字。

    empty_task类型的dummy是一个空任务,它是所有打印任务的父任务,我们要利用它的wait_for_all等待所有子任务完成。
    默认task的execute()执行完成后这个任务就会被删除,所在新建的所有printtask类型子任务不用显式地删除它们。
    同样,dummy一直没有执行,所以这个删除任务就由我们来处理。task必须使用destroy来删除。

    task还有一个特色是它只能使用placement new的形式新建任务,placement new中使用的是一个代理对象,这些代理对象可以是下面中的一个:
task::allocate_root() 生成根任务
this->allocate_continuation() 生成一个和当前任务同级的任务,并把当前任务的父任务转移过来。一般用于立即返回当前任务并由这个新任务代替当前任务继续做接下去的事。
this->allocate_child() 生成当前任务的子任务
this->task::allocate_additional_child_of(parent) 为指定的parent生成一个子任务

    本例中还用到了三个task的方法
  1. void set_ref_count(int count)
  2. void spawn(task& child)
  3. void wait_for_all()

    set_ref_count设置任务的参考计数,当任务中的一个子任务完成后参考计数减一
    spawn把子任务放入“就绪池”并马上返回
    wait_for_all等待任务的参考计数降到1为止(所以上面是:dummy->set_ref_count(10+1);),然后把这个计数值设为0。
    这里还有必要提一下,task还提供了一个spawn_and_wait_for_all(task& child)方法,相当于spawn(child);wait_for_all();。据说性能更高一点。

    spawn还支持一个叫 task_list的类作为参数,这是一个存放任务的队列,它只有四个方法:
  1. void push_back(task& task);
  2. task& pop_front();
  3. void clear();
  4. bool empty() const;

    看名字即可知道怎样使用它们了。比如上面的例子我们可以这样写:

  1. int main()
  2. {
  3.     task_scheduler_init init;
  4.  
  5.     task *dummy = new(task::allocate_root()) empty_task;
  6.     dummy->set_ref_count(10+1);
  7.  
  8.     task_list tl;
  9.     for(int i=0; i<10; i++)
  10.     {
  11.         task* childtask = new(dummy->allocate_child()) printtask(i);
  12.         // 把任务放入列表
  13.         tl.push_back(*childtask);
  14.     }
  15.     // 放入“就绪池”并等待
  16.     dummy->spawn_and_wait_for_all(tl);
  17.     dummy->destroy(*dummy);
  18.  
  19.     return 0;
  20. }

你可能感兴趣的:(多线程,编程,linux,windows)