一个使用TBB库的程序样子应该是这样地:
task_scheduler_init对象在构造时初始化TBB环境(比如线程池之类的东东),析构时回收TBB环境。在使用其它TBB组件之前必须先构造一个task_scheduler_init对象。
我们可以在task_scheduler_init对象的构造函数里指定线程池里线程的数量,比如tbb::task_scheduler_init init(10)。如果不指定,默认值是task_scheduler_init::automatic,它会自动根据当前系统决定线程量。
task_scheduler_init定义在tbb/task_scheduler_init.h文件中,绝大部分的TBB组件都放在它们自己的头文件中,比如下面要讲的blocked_range放在blocked_range.h中,parallel_for放在parallel_for.h中等。
为了突出TBB的简单易学,也为了增强一下学习的信心,先放上一个小甜饼:并行排序
从定义可以看出,它的用法与std::sort完全一样,我们只需把原程序里的std::sort替换成tbb::parallel_sort,就得到了一个多核优化的程序了(嗯~~起码能向别人这么吹了,呵呵)。
例:
注意,和std::sort一样,tbb::parallel_sort的排序结果是不稳定的。
比如我们的程序里有这样的代码
运行它要花费3秒的时间,顺序输出0123456789
现在,我们把它改成并行处理方式:
在我的双核CPU下,它的运行时间是1563ms,快了近一倍,输出是乱序的,说明循环被并行处理了。
template<Value>blocked_range模板类用于存放一对一维迭代器,这对分别指向起始和终止的迭代器组成了一个范围块。
它的构造函数是:
blocked_range( Value begin_, Value end_, size_type grainsize_=1 );
参数begin_和end_指定起始、终止迭代器,然后可通过begin()和end()方法得到它们。
第三个参数grainsize指定以多大粒度拆分范围。
在运行时,TBB会把blocked_range指定的范围拆分成几个小块来并行执行。上例中,TBB会生成复制出多个OpFor,然后把拆分出来的小块交给这些OpFor执行。由于我们使用默认拆分粒度1,则tbb::blocked_range<int>(0,10)会被拆分成10个小块交给10个OpFor执行(可以为OpFor加上拷贝构造和析构,看是不是调用了10次)。
parallel_for函数启动并行循环,上例中,一个更好的方式是:
tbb::parallel_for(tbb::blocked_range<int>(0,10), f, tbb::auto_partitioner());
其中第三个参数指定循环体的拆分方式,使用auto_partitioner()则会根据循环块的运算量和系统环境自动决定如何拆分,这是一个推荐的方式。默认是simple_partitioner(),它简单地按照blocked_range的第三个参数拆分循环。
注意,循环块必须是可重入的才可以用parallel_for来提升效率。如果一段代码,上一次运行和下一次运行之间没有联系,或者说上一次运行不会改变下一次运行的行为就是可重入的。比如:
下面这段代码for循环里的代码块是可重入的,前一次循环和下一次循环没有关系。
下面这段累加的代码是不可重入的,因为下一次循环依赖于上一次循环的结果(想象一下,如果i=5和i=6并行执行的情形,这个sum的值必然要出现混乱)。
上面提到一个不可重入的累加的例子,不能使用parallel_for并行执行,TBB用另一种方法让这种形式的循环也能并行执行。见下图:
把一个范围blocked_range切分成几份小块,并行地执行各小块中的累加操作,最后把各小块的累加结果合并起来得到最终结果。
代码:
本例中,parallel_reduce的用法和parallel_for相同。关键是运算子OpSum,它必须要有一个构造函数OpSum( OpSum& x, tbb::split)和一个方法void join(OpSum& x)。
当TBB决定分出一个blocked_range时,会调用OpSum( OpSum& x, tbb::split),这个tbb::split只是一个点位符,用于和拷贝构造函数相区别。
当TBB合并blocked_range时,会调用void join(OpSum& x),本例中合并就意味着把两个结果加起来。
在TBB的examples目录下有使用parallel_reduce并行查找素数的例子。
把一个范围blocked_range切分成几份小块,并行地执行各小块中的累加操作,最后把各小块的累加结果合并起来得到最终结果。
代码:
本例中,parallel_reduce的用法和parallel_for相同。关键是运算子OpSum,它必须要有一个构造函数OpSum( OpSum& x, tbb::split)和一个方法void join(OpSum& x)。
当TBB决定分出一个blocked_range时,会调用OpSum( OpSum& x, tbb::split),这个tbb::split只是一个点位符,用于和拷贝构造函数相区别。
当TBB合并blocked_range时,会调用void join(OpSum& x),本例中合并就意味着把两个结果加起来。
在TBB的examples目录下有使用parallel_reduce并行查找素数的例子。
流水线操作是常用的一种并行模式,它模仿传统的工厂装配线的工作模式。数据流过一系列的流水线节点,每个节点以自己的方式处理数据,然后传给下一节点。这里,有些节点可能可以并行操作,而有些不行。比如视频处理,有些节点对某帧的操作不依赖于其它帧的数据,这些节点就可以同时处理多个帧;而有些节点就需要等上一帧处理完再处理下一帧。
TBB的pipeline和filter类实现了这种流水线模式。
数据处理节点要从filter继承,filter的构造函数只有一个bool型的参数,指出是否是串行操作,如果是true,则此节点只能串行操作,如果是false,这个节点就可以并行操作了。
filter里有一个纯虚函数void* operator()(void* item),必须实现它。我们从输入参数item取得上一节点传入的数据,然后返回处理后的数据。
完成所有节点后,使用pipeline的add_filter方法按顺序加入这些节点,最后调用pipeline的run方法运行这个流水线,直到流水线的第一个节点返回NULL为止。
本例中,MyFilter1取得数据,并加了一个结束条件(取m_nCount次);然后把数据传给MyFilter2,MyFilter2处理时间比较长(每次操作要花1秒的时间),好在它能并行处理;最后传给MyFilter3输出。运行方式如下图
pipeline的run方法要指定最大可同时启动的任务数,上图中最大同时启动了5个任务(一种颜色表示一个任务)。要注意的是,实际的任务数还与task_scheduler_init初始定义的线程池容量有关,如我的双核CPU电脑上貌似默认的线程池里只有两个线程,要试验最大任务数的话可以直接指定线程池容量,如直接指定线程池里有20个线程:
tbb::task_scheduler_init init(20);
执行结果如下
MyFilter1:9f4;MyFilter2:9f4;MyFilter3:9f4
MyFilter1:b5c;MyFilter2:b5c;MyFilter3:9f4
MyFilter1:b5c;MyFilter2:b5c;MyFilter3:b5c
MyFilter1:9f4;MyFilter2:9f4;MyFilter3:9f4
MyFilter1:b5c;MyFilter2:b5c;MyFilter3:b5c
MyFilter1:9f4;MyFilter2:9f4;MyFilter3:9f4
MyFilter1:b5c;MyFilter2:b5c;MyFilter3:b5c
MyFilter1:9f4;MyFilter2:9f4;MyFilter3:9f4
MyFilter1:b5c;MyFilter2:b5c;MyFilter3:b5c
MyFilter1:9f4;MyFilter2:9f4;MyFilter3:9f4
转自http://www.cppprog.com/2009/0325/92_4.html