TBB提供几个并发模型中,最与众不同的是pipeline模型.
Intel曾经在NP IXP 2400平台上实现过pipeline模型.不过是硬件实现.
这次TBB是纯软件实现.
对于这种模型,优点和缺点都很明显.
优点: 模块划分清晰简介,代码量少,便于单个模块测试.在硬件线程多于32个系统中,pipeline是最高效的.因为,当硬件线程多了以后,锁冲突的可能性大大增加,pipeline模型将临界资源分而治之的思想,可以最小化锁冲突.
缺点:在硬件线程少于4个的时候,效率不及常见的模型.
基于以上两点,可以说pipeline是未来多核(>32)软件的主流模型.
说了这麽多废话,还是来看看到底什么是Pipeline.
从名字上,就可以看出Pipeline是借鉴于工业上的流水线模型,将一个功能(大于模块级的功能),分解成多个独立的阶段,不同阶段间通过队列传递产品.这样子,对于一些CPU密集和IO密集的应用,通过Pipeline模型,我们可以把CPU密集stage放在一个filter, 將IO密集stage放在另外一个filter.两个filter可以分配不同的线程,通过连接两者的队列匹配两者的速度差异,从而达到最好的并发效率.
听上去有点负责吧?
不过,TBB帮我们做了很多自动化的操作,上面提到的线程和队列都是不需要使用者去关心的.只需要简单的几个步骤就好.下面来看一个例子:(源自Intel的TBB的自带实例)
这是一个读写文件的例子:
... {
FILE*input_file=fopen(InputFileName,"r");
FILE*output_file=fopen(OutputFileName,"w");
//创建一个pipeline
tbb::pipelinepipeline;
//创建文件读stage并添加到pipeline
MyInputFilterinput_filter(input_file);
pipeline.add_filter(input_filter);
//创建处理stage并添加到pipeline
MyTransformFiltertransform_filter;
pipeline.add_filter(transform_filter);
//创建文件写stage并添加到pipeline
MyOutputFilteroutput_filter(output_file);
pipeline.add_filter(output_filter);
//运行pipeline
tbb::tick_countt0=tbb::tick_count::now();
pipeline.run(MyInputFilter::n_buffer);
tbb::tick_countt1=tbb::tick_count::now();
pipeline.clear();
fclose(output_file);
fclose(input_file);
}
下面看一下文件读stage的实现:
public:
staticconstsize_tn_buffer=8;//读文件缓冲块个数
MyInputFilter(FILE*input_file_);
private:
FILE*input_file;
size_tnext_buffer;
charlast_char_of_previous_buffer;//溢出标志,buffer[0][-1]访问,
MyBufferbuffer[n_buffer];//就是一个捡简单的buffer类,这里不是重点
/**//*override*/void*operator()(void*);//当前stage的运行入口,注意,参数是void*,这个参数是上一级的输出,
//在这里读文件是第一个stage,所以不需要处理参数.
} ;
MyInputFilter::MyInputFilter(FILE * input_file_):
filter( /**/ /*is_serial=*/ true ), // 这里指定当前的stage是否可以并行化,true表示串行
next_buffer( 0 ),
input_file(input_file_),
last_char_of_previous_buffer( ' ' )
... {
}
void * MyInputFilter:: operator ()( void * ) ... {
MyBuffer&b=buffer[next_buffer];
next_buffer=(next_buffer+1)%n_buffer;//循环使用文件读缓冲区
size_tn=fread(b.begin(),1,b.max_size(),input_file);
if(!n)...{
//文件结束
returnNULL;
}else...{
b.begin()[-1]=last_char_of_previous_buffer;//设置溢出标志,这是一个小技巧
last_char_of_previous_buffer=b.begin()[n-1];//保存当前块的最后一个字符
b.set_end(b.begin()+n);
return&b;
}
}
注意:这里的[-1]下标就是访问数组前一个单元的意思,这个不是重点.
可以看到,读文件操作是串行的在一个stage里完成的.
再来看看转换stage:
class MyTransformFilter: public tbb::filter ... {
public:
MyTransformFilter();
/**//*override*/void*operator()(void*item);//这个item正是上一个stage的返回值,MyBuffer对象的指针
} ;
MyTransformFilter::MyTransformFilter():
tbb::filter( /**/ /*is_serial_=*/ false )
... {} // false标志可以并行的stage
/**/ /*override*/ void * MyTransformFilter:: operator ()( void * item) ... {
//这个函数是可并发的,但是却没有显示的锁,
//这就是pipeline模式的精髓
//同步被隐藏在队列操作中,只要不操作外部数据结构,
//在只操作void*的前提下,无需显示同步处理
MyBuffer&b=*static_cast<MyBuffer*>(item);
intprev_char_is_space=b.begin()[-1]=='';
for(char*s=b.begin();s!=b.end();++s)...{
if(prev_char_is_space&&islower(*s))//转换首字母到大写
*s=toupper(*s);
prev_char_is_space=isspace((unsignedchar)*s);
}
return&b;//继续传给下一个stage
}
最后是写文件的stage:
FILE*my_output_file;
public:
MyOutputFilter(FILE*output_file);
/**//*override*/void*operator()(void*item);
} ;
MyOutputFilter::MyOutputFilter(FILE * output_file):
tbb::filter( /**/ /*is_serial=*/ true ), // 串行的
my_output_file(output_file)
... {
}
void * MyOutputFilter:: operator ()( void * item) ... {
MyBuffer&b=*static_cast<MyBuffer*>(item);
fwrite(b.begin(),1,b.size(),my_output_file);
returnNULL;
}
MyBuffer类:
staticconstsize_tbuffer_size=10000;
char*my_end;
//!storage[0]保存前一个buffer的最后一个字符
charstorage[1+buffer_size];
public:
//!Pointertofirstcharacterinthebuffer
char*begin()
...{
returnstorage+1;//跳过[0]
}
constchar*begin()const...{returnstorage+1;}
//!最后一个字符的下一个位置
char*end()const...{returnmy_end;}
//!设置my_end
voidset_end(char*new_ptr)...{my_end=new_ptr;}
//!最大容量
size_tmax_size()const...{returnbuffer_size;}
//!已经保存的字符数
size_tsize()const...{returnmy_end-begin();}
} ;
可以看出, Pipeline模型的思想是面向 步骤(stage)的 ,不同的stage封装成不同的filter可以灵活的动态的安装在流水线上.实现了高可拆卸性.
在效率方面, Pipeline模型的精髓在于隐藏同步到队列边界.
而队列的缓冲作用减少了锁碰撞的负面影响.从而实现高效方便的并发编程模型.
呵呵,先到这里.