TBB(Intel Threading Building Blocks)學習筆記並行與併發是相對的,OS裏講的是併發而在架構方面更多的是說並行。並行是分多個層面的,個人認爲基本上可以分爲這麼幾個層面:1、指令級的並行;即所謂的微程序、指令流水線等,現在cpu的一級緩存、二級緩存都很大,所以這個cache的效果還是比較好的(基於局部性原理)2、線程級的並行;即同一個時刻多個函數在運行(現在的cpu好像都是多核的)3、服務級別的(比如一個遊戲服務器中有商店服務、也有戰鬥服務、聊天服務等 這裏的每個服務可能對應多個邏輯線程)4、節點級別的;即所謂的分佈式系統,多個節點互相配合,使整個系統在邏輯上成爲一個單一的系統。(google、qq等這些海量訪問的服務統統是分佈式的)一般來說,第一個級別的並行直接做在硬件裏面,第二個級別的並行會有一些基礎的框架,第三和第四個級別的並行就是應用程序自己的架構的問題了。這裏面實際上有一個爭論:是在算法並行化上面花心思去研究還是採用分佈式的框架來面對問題規模的增長?實際上2者各有利弊,前者可以充分利用已有硬件,但是對程序員的要求較高,維護開發成本高,風險大;後者容易實現但是浪費硬件,在有些情況下不是所有問題都可以用加個機器的方式可以解決的(比如客戶端上的多媒體軟件,其計算量極大,總不能要求所有用戶都升級吧。)
Intel Threading Building Blocks,是爲了方便程序員使用多核處理器的C++庫,應該是對應上面提到的第二個級別的並行的。
一、TBB應該提供哪些東西?
用TBB就是爲了程序的並行化,那麼程序員需要什麼樣的支持呢?最理想的情況是已有的代碼不作任何修改,換一個編譯器重新編譯一下就OK,現在看來這個還不太現實。要有更好的效率就需要有更多的啓發式信息,同時也就要求程序員要了解很多細節。整個程序邏輯沒辦法自動並行化,那就針對控制流進行並行化吧,所以TBB中提供了 parallel_for、parallel_while、 parallel_reduce等;(這些是TBB給C++程序員的比較高層的接口)並行肯定是多線程,這樣的話數據競爭問題就比較棘手,所以TBB提供併發容器;如果覺得
TBB提供的這些接口還沒有辦法解決性能問題,那就可以更深入的研究使用mutex、atomic、task等了;可以看出,TBB從幾個層次上爲程序員提供了支持。
二、TBB提供的接口
由底層到高層,task_scheduler--------concurrent_container--------parallel_for---pipeline
簡單說,TBB幫我們調度一個個task(比OS的調度要高效),實現高效的並行算法
三、細節
1、parallel_for 適用場合:多個數據或請求彼此沒有依賴關係,所要進行的操作是一樣的(典型SPMD)
例子:
// 典型的c++泛型編程 blocked_range 是要處理的多個數據,3個參數依次是開始的指針(迭代器)、結束指針、每個任務分配的數據數
// parallel_forFibBody可以簡單理解爲一個函數對象(c++裏是用運算符重載實現的,即()是通信的接口)
parallel_for( blocked_range<int>( 1, my_n, 10 ), parallel_forFibBody(my_stream) );
struct parallel_forFibBody {
QueueStream &my_stream;
//! fill functor arguments
parallel_forFibBody(QueueStream &s) : my_stream(s) { }
// 這裏是並行的代碼
void operator()( const blocked_range<int> &range ) const {
int i_end = range.end();
for( int i = range.begin(); i != i_end; ++i ) {
my_stream.Queue.push( Matrix1110 ); // push initial matrix
}
}
};
2、parallel_reduce 適合於需要彙總的情況,即各個數據的結果需要彙總回來
例子:(注意分發下去和彙總回來的方法)
float ParallelSumFoo( const float a[], size_t n ) {
SumFoo sf(a);
parallel_reduce(blocked_range<size_t>(0,n,IdealGrainSize), sf );
return sf.sum;
}
class SumFoo {
float* my_a;
public:
float sum;
void operator()( const blocked_range<size_t>& r ) {
float *a = my_a;
for( size_t i=r.begin(); i!=r.end(); ++i )
sum += Foo(a[i]);
}
SumFoo( SumFoo& x, split ) : my_a(x.my_a), sum(0) {} // 分發任務,注意這個構造器要求是線程安全的
void join( const SumFoo& y ) {sum+=y.sum;} // 收集彙總結果
SumFoo(float a[] ) :
my_a(a), sum(0)
{}
};
3、parallel_while 有時不知道循環何時結束,即使用for的end未知,在這種情況下可以使用parallel_while
例子:注意pop_if_present、typedef Item* argument_type、operator()等部分的處理
// 串行版本
void SerialApplyFooToList( Item*root ) {
for( Item* ptr=root; ptr!=NULL; ptr=ptr->next )
Foo(pointer->data);
}
// 並行版本
class ItemStream {
Item* my_ptr;
public:
bool pop_if_present( Item*& item ) { // 用於提供下一個迭代器
if( my_ptr ) {
item = my_ptr;
my_ptr = my_ptr->next;
return true;
} else {
return false;
}
};
ItemStream( Item* root ) : my_ptr(root) {}
}
class ApplyFoo {
public:
void operator()( Item* item ) const { // 要求一定是const的
Foo(item->data);
}
typedef Item* argument_type; // 此句是必須的
};
void ParallelApplyFooToList( Item*root ) {
// parallel_while是個class
parallel_while<ApplyFoo> w; // 先建立個對象
ItemStream stream;
ApplyFoo body;
// 第一個參數提供數據指針,第二個參數提供函數體
w.run( stream, body );
}
四、併發容器
大部分程序都有容器類,在多線程環境下就有數據污染的問題,爲了使併發的線程串行化,一般是使用加鎖的辦法,如果這個
容器由程序員自己來實現,難度還是比較大的,這樣就需要有線程安全的容器類。
1、concurrent_hash_map
hash接口與stl類似
2、concurrent_vector
grow_by(n) 插入n個item(動態增長)
grow_to_at_least()設定容器的大小
size() 包括正在併發增長的部分 因爲有可能會同時取,所以程序員需要自己維護自己的class的線程安全性
clear() 不是線程安全的
3、concurrent_queue
pop_if_present(item) 非阻塞,
pop() 阻塞,
concurrent_queue::size() 負數時表示有多少個消費者在等待
set_capacity()指定隊列大小,會使push操作被阻塞
在並行時,paralell_while pipeline 的效率要高於concurrent_queue
五、如果覺得TBB的加鎖效率不高,可以自己控制鎖
最常用的是spin lock
六、整個TBB引擎的核心是 Task Scheduler(基於任務圖來實現)
提高效率的核心是threading stealing,保證cpu的效率
七、小結
要使用TBB進行並行化,首先程序員要知道哪些是可以並行化;其次,要熟悉TBB並行化的框架(主要是泛型編程);再次,程序員要大概知道
並行算法的執行步驟;最後,利用TBB的組件,實現並行化的算法。總體上來說,還是不太好用的