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