学习英特尔线程构建模块开源2.1库
并行編程是未來,但是您如何才能有效利用多核CPU的高性能并行編程呢? 當然,也可以選擇使用諸如POSIX線程之類的線程庫,但是最初是出于C語言引入POSIX線程框架的。 這也是一種太底層的方法,例如,您無權訪問任何并發容器,也沒有任何可使用的并發算法。 在這一點上,英特爾推出了英特爾?線程構建塊(Intel TBB),這是一種基于C++的并行編程框架,具有許多有趣的功能,并且比線程具有更高的抽象水平。
常用縮略語
- POSIX: UNIX的便攜式操作系統接口
下載和安裝Intel TBB并不需要什么特別的事情:提取的目錄層次結構讓人聯想到帶有include,bin,lib和doc文件夾的UNIX?系統。 出于本文的目的,我選擇了tbb30_20110427oss穩定版本。
英特爾TBB入門
英特爾TBB有很多工作要做。 以下是一些有趣的入門知識:
- 您可以在任務中擁有更高級別的抽象,而不是線程。 英特爾聲稱,在Linux?系統上,啟動和終止任務比啟動和停止線程快18倍。
- 英特爾TBB帶有任務計劃程序,該任務計劃程序可以有效處理多個邏輯和物理內核之間的負載平衡。 Intel TBB中的默認任務調度策略與大多數線程調度程序具有的循環策略不同。
- 英特爾TBB提供現成的貨架線程安全的容器,如可用性concurrent_vector和concurrent_queue 。
- 可以使用諸如parallel_for和parallel_reduce類的通用并行算法。
- 模板類atomic提供了無鎖 (也稱為無互斥 )并發編程支持。 這種支持使Intel TBB適用于高性能應用程序,因為Intel TBB可以處理互斥鎖。
- 全部都是C++ ! 由于沒有花哨的擴展名或宏,英特爾?TBB仍停留在該語言之內,從而大量使用了模板。
英特爾TBB確實有很多先決條件。 在開始之前,您應該具備:
- C++模板以及對標準模板庫(STL)的一些理解。
- 線程知識-POSIX線程或Windows?線程。
盡管不是必需的,但C++0x lambda函數在Intel TBB中找到了相當的用法。
英特爾TBB的討論始于創建和處理任務和同步原語(mutex),然后使用并發容器和并行算法。 它以使用原子模板的無鎖編程結束。
您好,World with Intel TBB任務
英特爾TBB基于任務的概念。 您定義自己的任務,這些任務派生自tbb / task.h中聲明的tbb::task 。 要求用戶在其代碼中覆蓋純虛擬方法task* task::execute ( ) 。 以下是每個Intel TBB任務的一些屬性:
- 當Intel TBB任務計劃程序選擇運行某些任務時,將調用任務的execute方法。 那是切入點。
- execute方法可以返回task* ,它告訴調度程序下一個要運行的任務。 如果返回NULL,則調度程序可以自由選擇下一個任務。
- task::~task( )是虛擬的,并且用戶任務分配的任何資源都必須在此析構函數中釋放。
- 通過調用task::allocate_root( ) 。
- 主任務通過調用task::spawn_root_and_wait(task)來運行任務以完成task::spawn_root_and_wait(task) 。
下面的清單1顯示了第一個任務及其調用方式:
清單1.創建第一個英特爾TBB任務
#include "tbb/tbb.h" #include <iostream> using namespace tbb; using namespace std;class first_task : public task { public: task* execute( ) { cout << "Hello World!\n";return NULL;} };int main( ) { task_scheduler_init init(task_scheduler_init::automatic);first_task& f1 = *new(tbb::task::allocate_root()) first_task( );tbb::task::spawn_root_and_wait(f1); }要運行Intel TBB程序,必須正確初始化任務計劃程序。 清單1中調度程序的參數是自動的,它使調度程序可以自己決定線程數。 當然,如果要控制產生的最大線程數,則可以覆蓋此行為。 但是在生產代碼中,除非您真的知道自己在做什么,否則最好將確定最佳線程數的工作留給調度程序。
現在您已經創建了第一個任務,讓清單1中的first_task產生一些子任務。 下面的清單2引入了一些新概念:
- 英特爾TBB提供了一個名為task_list的容器,該容器旨在用作任務的集合。
- 每個父任務都使用allocate_child函數調用創建一個子任務。
- 在任務產生任何子任務之前,它必須調用set_ref_count 。 否則會導致未定義的行為。 如果要生成子任務,然后等待它們完成,則count必須等于子任務的數量+ 1; 否則, count應等于子任務數。 不久之后會更多。
- 對spawn_and_wait_for_all的調用如其名稱所示:產生子任務并等待所有操作完成。
這是代碼:
清單2.創建多個子任務
#include "tbb/tbb.h" #include <iostream> using namespace tbb; using namespace std;class first_task : public task { public: task* execute( ) { cout << "Hello World!\n";task_list list1; list1.push_back( *new( allocate_child() ) first_task( ) );list1. push_back( *new( allocate_child() ) first_task( ) );set_ref_count(3); // 2 (1 per child task) + 1 (for the wait) spawn_and_wait_for_all(list1);return NULL;} };int main( ) { first_task& f1 = *new(tbb::task::allocate_root()) first_task( );tbb::task::spawn_root_and_wait(f1); }那么,為什么英特爾TBB要求顯式設置set_ref_count ? 該文檔說,這主要是出于性能方面的考慮。 生成子代之前,必須始終為任務設置引用計數。 請參閱相關信息的鏈接,更多的細節。
您也可以創建任務組。 以下代碼創建一個任務組,該任務組產生兩個任務并等待它們完成。 task_group的run方法具有以下簽名:
template<typename Func> void run( const Func& f )run方法產生一個可計算f( )但不會阻塞調用任務的任務,因此控件會立即返回。 為了等待子任務完成,調用任務調用wait (請參見下面的清單3 )。
清單3.創建一個task_group
#include "tbb/tbb.h" #include <iostream> using namespace tbb; using namespace std;class say_hello( ) { const char* message;public: say_hello(const char* str) : message(str) { }void operator( ) ( ) const { cout << message << endl;} };int main( ) { task_group tg;tg.run(say_hello("child 1")); // spawn task and returntg.run(say_hello("child 2")); // spawn another task and return tg.wait( ); // wait for tasks to complete }請注意task_group的語法簡單性-直接處理任務時不需要進行內存分配等調用,并且您無需對ref計數做任何事情。 這就是任務。 英特爾TBB任務可以完成數百種事情。 請確保深入了解Intel TBB文檔以獲取更多詳細信息。 讓我們繼續并發容器。
并發容器:矢量
現在,讓我們集中討論Intel TBB的并發容器之一: concurrent_vector 。 此容器在標頭tbb / concurrent_vector.h中聲明,并且基本接口類似于STL向量:
template<typename T, class A = cache_aligned_allocator<T> > class concurrent_vector;可以將多個控制線程安全地添加到向量中,而無需任何顯式鎖定。 從英特爾TBB手動意譯, concurrent_vector具有以下特性:
- 它提供對元素的隨機訪問; 索引從位置0開始。
- 安全并發增加大小是可能的,并且可以同時添加多個線程。
- 添加新元素不會使現有索引或迭代器無效。
但是,并發是有代價的。 與STL不同,在STL中,添加新元素涉及數據的移動,而concurrent_vector數據不移動。 而是,容器維護一系列連續的內存段。 顯然,這增加了容器開銷。
對于同時添加向量,可以使用三種方法:
- push_back在向量的末尾附加一個元素。
- grow_by(N) -append N型的連續元素T到concurrent_vector和迭代器返回到第一所附元件。 每個元素都用T ( )初始化。
- grow_to_at_least(N) -Grow載體以大小為N,如果向量的當前大小小于N。
您可以按如下所示將字符串附加到concurrent_vector :
void append( concurrent_vector<char>& cv, const char* str1 ) { size_t count = strlen(str1)+1; std::copy( str1, str1+count, cv.grow_by(count) ); }借助英特爾?TBB立即使用并行算法
關于Intel TBB的最好的事情之一是,它使您可以自動并行化部分源代碼,而不必費心創建和維護線程。 最常見的并行算法是parallel_for 。 考慮以下示例:
void serial_only (int* array, int size) { for (int count = 0; count < size; ++count)apply_transformation (array [count]); }現在,如果上一apply_transformation中的apply_transformation例程沒有做任何奇怪的事情,例如僅對單個數組元素進行了一些轉換,那么您就無法阻止將負載分配給多個CPU內核。 您需要英特爾TBB庫中的兩個類才能入門: blocked_range (來自tbb / blocked_range.h)和parallel_for (來自tbb / parallel_for.h)。
blocked_range類旨在創建一個向迭代器提供parallel_for的對象,因此您需要創建諸如blocked_range (0, size) ,并將其作為輸入傳遞給parallel_for 。 parallel_for需要的第二個也是最后一個參數是具有清單4中的要求的類(從parallel_for.h標頭粘貼)。
清單4. parallel_for的第二個參數的要求
/** \page parallel_for_body_req Requirements on parallel_for bodyClass \c Body implementing the concept of parallel_for body must define:- \code Body::Body( const Body& ); \endcode Copy constructor- \code Body::~Body(); \endcode Destructor- \code void Body::operator()( Range& r ) const; \endcode Function call operator applying the body to range \c r. **/該代碼告訴您,您需要使用operator ( )創建自己的類,并使用blocked_range作為參數,并在operator ( )的方法定義內對您先前創建的serial for循環進行編碼。 復制構造函數和析構函數應該是公共的,并且您讓編譯器為您提供默認值。 下面的清單5顯示了代碼。
清單5.為parallel_for創建第二個參數
#include "tbb/blocked_range.h" using namespace tbb;class apply_transform{ int* array; public: apply_transform (int* a): array(a) {} void operator()( const blocked_range& r ) const { for (int i=r.begin(); i!=r.end(); i++ ){ apply_transformation(array[i]); } } };現在您已經成功創建了第二個對象,您只需調用parallel_for ,如清單6所示。
清單6.使用parallel_for并行化循環
#include "tbb/blocked_range.h" #include "tbb/parallel_for.h" using namespace tbb;void do_parallel_the_tbb_way(int *array, int size) { parallel_for (blocked_range(0, size), apply_transform(array)); }英特爾TBB中的其他并行算法
英特爾TBB提供了很多并行算法,例如parallel_reduce (在tbb / parallel_reduce.h中聲明)。 假設您要匯總所有元素,而不是對每個單獨的數組元素應用轉換。 這是序列號:
void serial_only (int* array, int size) { int sum = 0;for (int count = 0; count < size; ++count)sum += array [count]; return sum; }從概念上講,在并行上下文中運行此代碼將意味著每個控制線程都應匯總數組的某些部分,并且必須在某處存在join方法來匯總部分求和。 下面的清單7顯示了Intel TBB代碼。
清單7.串行for循環求和數組元素
#include "tbb/blocked_range.h" #include "tbb/parallel_reduce.h" using namespace tbb;float sum_with_parallel_reduce(int*array, int size) {summation_helper helper (array); parallel_reduce (blocked_range<int> (0, size, 5), helper);return helper.sum; }在將每個線程的數組拆分為子數組時,您需要保持一定的粒度(例如,每個線程負責對N個元素求和,其中N既不太大也不不太小)。 那是blocked_range的第三個參數。 英特爾TBB要求summation_helper類滿足兩個條件:它必須具有一個名為join的方法以添加部分和和以及一個帶有特殊參數的構造函數(稱為splitting構造函數 )。 清單8提供了代碼:
清單8.使用join方法創建summation_helper類并拆分構造函數
class summation_helper {int* partial_array; public:int sum;void operator( )( const blocked_range<int>& r ) {for( int count=r.begin(); count!=r.end( ); ++count)sum += partial_array [count];}summation_helper (summation_helper & x, split): partial_array (x. partial_array), sum (0) {}summation_helper (int* array): partial_array (array), sum (0){}void join( const summation_helper & temp ) { sum += temp.sum; // required method } };這就是將會發生的事情。 Intel TBB調用splitting構造函數(稱為split的第二個參數是Intel TBB所需的虛擬參數),并且部分數組由一定數量的元素填充(該數量是blocked_range定義的粒度的函數)。 當子數組上的求和完成時, join方法將添加部分結果。 有點復雜? 乍看起來也許; 只需記住您需要三個方法: operator( )添加數組范圍, join添加以添加部分結果,以及split構造函數以啟動新的工作線程。
英特爾TBB還有其他幾種有用的算法, parallel_sort是最有用的算法之一。 請參閱英特爾TBB參考手冊(見相關信息 )了解詳情。
使用Intel TBB進行無鎖編程
在多線程編程期間經常出現的一個問題是,互斥鎖的鎖定和解鎖浪費了CPU周期數。 如果您來自POSIX線程背景,英特爾TBB的atomic模板將使您感到驚訝。 它是互斥鎖的替代方法,速度要快得多,并且您可以放心地取消對鎖定和解鎖代碼的需求。 atomic是所有編碼難題的靈丹妙藥嗎? 否。它的使用受到嚴格限制。 但是,如果您要創建高性能代碼,這將非常有效。 聲明整數為atomic類型的方法如下:
#include "tbb/atomic.h" using namespace tbb;atomic<int> count; atomic<float* > pointer_to_float;現在,假設多個控制線程正在訪問較早版本的變量計數。 通常,您希望在寫入過程中使用互斥量來保護計數; 但是, atomic<int>不再需atomic<int> 。 看一下清單9 。
清單9.原子fetch_and_add不需要鎖定
// writing with mutex, count is declared as int count; {// … codepthread_mutex_lock (&lock);count += 1000; pthread_mutex_unlock (&lock);// … code continues }// writing without mutex, count declared as atomic<int> count; {// … codecount.fetch_and_add (1000); // no explicit locking/unlocking// … code continues }代替+= ,可以使用atomic<T>類的fetch_and_add方法。 不,它在內部不使用任何互斥鎖作為fetch_and_add方法的一部分。 當執行fetch_and_add ,它的作用是立即加1000以立即count -所有線程一次都可以看到count的更新值,或者所有線程都可以繼續看到舊值。 這就是為什么count被聲明為atomic變量:對操作count是原子,并且不能按照進程或線程調度的反復無常而中斷。 無論如何調度線程,都無法在不同線程中count不同的值。 有關無鎖編程的深入討論,請參閱參考資料 。
atomic<T>類具有以下五個基本操作:
y = x; // atomic read x = b; // atomic write x.fetch_and_store(y); // y = x and return the old value of x x.fetch_and_add(y); // x += y and return the old value of x x.compare_and_swap(y, z); // if (x == z) x = y; in either case, return old value of x另外,為方便起見,支持運算符+= , -= , ++和-- ,但它們都在fetch_and_add之上fetch_and_add 。 如tbb / atomic.h所示,這是定義運算符的方式( 清單10 )。
清單10.使用fetch_and_add定義的運算符++,-,+ =和-=
value_type operator+=( D addend ) {return fetch_and_add(addend)+addend; }value_type operator-=( D addend ) {// Additive inverse of addend computed using binary minus,// instead of unary minus, for sake of avoiding compiler warnings.return operator+=(D(0)-addend); }value_type operator++() {return fetch_and_add(1)+1; }value_type operator--() {return fetch_and_add(__TBB_MINUS_ONE(D))-1; }value_type operator++(int) {return fetch_and_add(1);}value_type operator--(int) {return fetch_and_add(__TBB_MINUS_ONE(D));}請注意, atomic<T>中的類型T只能是整數類型,枚舉類型或指針類型。
結論
不可能在一篇文章中公正地描述一個具有英特爾TBB規模的圖書館。 確實,英特爾網站上有數十篇文章重點介紹了英特爾TBB的多個方面。 取而代之的是,本文試圖深入了解Intel TBB隨附的一些引人注目的功能-任務,并發容器,算法以及創建無鎖代碼的方法。 希望本文的介紹激發了您的興趣,英特爾TBB將獲得另一個熱情的用戶—就像作者本人一樣。
翻譯自: https://www.ibm.com/developerworks/aix/library/au-intelthreadbuilding/index.html
總結
以上是生活随笔為你收集整理的学习英特尔线程构建模块开源2.1库的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: firefox浏览器的onblur事件
- 下一篇: 梅花雨日期控件