遇見C++ PPL:C++ 的並行和非同步

來源:互聯網
上載者:User

遇見C++ PPL:C++ 的並行和非同步

 

Written by Allen Lee

 

You held it all, but you were careless to let it fall. You held it all, and I was by your side powerless.
– Linkin Park, Powerless

 

並行計算正弦值

      假設我們有一個數組,裡麵包含一組隨機產生的浮點數,現在要計算每個浮點數對應的正弦值,如果你看過我的《遇見C++ Lambda》,你可能會想到用for_each函數,如代碼1所示。為了可以把數組裡的浮點數替換成對應的正弦值,我們需要把Lambda的參數聲明為引用,如果你想保留那些浮點數,可以建立一個新的數組存放計算結果。

代碼 1

      值得提醒的是,這裡使用begin和end兩個函數分別擷取數組的起止位置,這是C++ 11的推薦寫法。此前,我們使用STL容器的begin和end兩個成員函數分別擷取起止位置,但這種做法無法覆蓋C風格數組;如今,C++ 11通過begin和end兩個函數把擷取C風格數組和STL容器的起止位置的寫法統一起來,不難想象,遵循新的寫法可以提高代碼的一致性。

      STL提供的for_each函數是串列執行的,如果你想充分利用多核的優勢,可以考慮換用PPL(Parallel Patterns Library)提供的parallel_for_each函數,整個改造過程只需三步:

  1. #include <ppl.h>
  2. using namespace concurrency;
  3. 把for_each改為parallel_for_each,如代碼2所示

代碼 2

      需要說明的是,如果你在Visual C++ 2010上使用PPL,你需要引用Concurrency命名空間(首字母大寫),這裡引用的concurrency命名空間(全小寫)是Visual C++ 2012的PPL為了和其他常見的全小寫命名空間(如stl)保持一致而建立的命名空間別名。

      如果你不想影響那些浮點數,可以建立一個新的數組,然後通過parallel_for函數把計算結果對應地存到新的數組裡,如代碼3所示。這裡選擇parallel_for函數主要是為了藉助索引管理兩個數組的元素的對應關係,如果你要在多個數組之間周旋,比如說,你要為A、B、C和D四個集合實現對應元素的 (A + B) / (C - D) 操作,那麼使用parallel_for函數就會非常直觀。

代碼 3

      對於我們這裡的簡單需求,如果你不想自己管理元素的對應關係,可以考慮parallel_transform函數,如代碼4所示。parallel_transform函數的前兩個參數指定輸入容器的起止位置,第三個參數指定輸出容器的開始位置,前兩個參數指向的位置之間的元素個數必須小於或等於第三個參數指向的位置和輸出容器的結束位置之間的元素個數,否則將會出錯。

代碼 4

 

並行數奇數個數

      在《遇見C++ Lambda》裡,我們通過for_each函數數一下隨機產生的整數裡有多少個奇數,這個過程可以並行化嗎?可以的,一般的做法是聲明一個變數存放個數,在迭代的過程中一旦發現奇數就遞增一下這個變數,由於涉及到多線程,可以通過系統提供的InterlockedIncrement函數確保遞增操作的安全,如代碼5所示。

代碼 5

      上面的代碼可以得到正確結果,但存在一個問題,每次發現奇數都要調用InterlockedIncrement函數,如果nums數組裡的奇數佔大多數,那麼調用InterlockedIncrement函數帶來的開銷可能會抵消並行帶來的好處,最終導致執行效率甚至比不上串列版本。為了避免這種影響,我們可以把volatile變數和InterlockedIncrement函數的組合寫法替換成PPL提供的combinable對象,如代碼6所示。

代碼 6

      combinable對象是如何協助parallel_for_each函數提高執行效率的呢?這個需要稍微瞭解一下parallel_for_each函數的工作方式,簡單的說,它會把我們傳給它的資料分成N塊,分別交給N個線程平行處理,但同一塊資料會在對應的線程裡串列處理,這意味著處理同一塊資料的代碼可以直接實現同步,combinable對象正是利用這點減少不必要的同步,從而提高parallel_for_each函數的執行效率。

      combinable對象會為每個線程提供一個線程局部儲存(Thread-Local Storage),每個線程局部儲存都會使用建立對象時提供的Lambda進行初始化。我們可以通過local成員函數訪問當前線程的線程局部儲存,因為combinable對象保證local成員函數返回的對象一定是當前線程的,所以我們可以放心的直接操作。當每個線程的操作都完成之後,我們就可以調用combine成員函數把每個線程局部儲存的結果匯總起來,這個時候會產生線程之間的同步,但同步工作由combinable對象負責,無需我們費心,我們只需告訴它匯總的方法就行了,在我們的樣本裡,這個邏輯是STL提供的plus函數對象。

      parallel_for_each函數和combinable對象的組合寫法本質上就是一個Reduce過程,PPL提供了一個parallel_reduce函數專門處理這類需求,如代碼7所示,它非常直接地展示了parallel_for_each函數和combinable對象隱藏起來的二段處理過程。

代碼 7

      第一個階段,parallel_reduce函數會把我們傳給它的資料分成N塊,分別交給N個線程平行處理,每個線程執行的代碼由第四個參數指定。在我們的樣本裡,這個參數是一個Lambda,parallel_reduce函數會通過Lambda的參數告訴我們每塊資料的起止位置,以及計算的初始值,這個初始值其實來自parallel_reduce函數的第三個參數,而Lambda的函數體則是不折不扣的串列代碼。所有線程執行完畢之後就會進入第二個階段,匯總每個線程的執行結果,匯總的方法由第五個參數指定。

      parallel_reduce函數和前面提到的parallel_transform函數可以組合起來實現並行MapReduce操作,而STL提供的transform和accumulate兩個函數則可以組合起來實現串列MapReduce操作。

 

同時執行不同任務

      假設我們現在的任務是計算一組隨機整數裡的所有奇數之和與第一個素數的商,一般的做法是按順序執行以下步驟:

  1. 產生一組隨機整數
  2. 計算所有奇數的和
  3. 找出第一個素數
  4. 計算最終結果

由於第二、三步是相互獨立的,它們只依賴於第一步的結果,我們可以同時執行這兩步提高程式的整體執行效率。那麼,如何同時執行兩個不同的代碼呢?可以使用parallel_invoke函數,如代碼8所示。

代碼 8

      parallel_invoke函數最多可以接受十個參數,換句話說,它可以同時執行最多十個不同的代碼,如果我們需要同時執行超過十個代碼呢?這個時候我們可以考慮建立一個Lambda數組,然後交給parallel_for_each/parallel_for函數去執行,如代碼9所示。

代碼 9

      這些代碼都能得到正確的結果,但它們都有一個缺點——阻塞當前線程。想想看,一般需要動用並行編程的地方都是計算量比較大的,如果要等它們算好才能繼續,恐怕會把使用者惹毛,但是,如果不等它們算好,後面的步驟可能沒法正常運作,怎麼辦呢?

 

async + continuation

      我們可以通過task對象非同步執行第一步,然後通過continuation把後續步驟按照既定的順序連結起來,這樣既可避免阻塞當前線程,又能確保正確的執行順序。

      首先,把各個步驟需要共用的變數挪到前面,如代碼10所示,這些變數將被對應的步驟捕獲並使用。

代碼 10

      然後,通過create_task函數建立一個task對象,非同步執行第一步,如代碼11所示。create_task函數負責用我們傳給它的Lambda建立task對象,這個Lambda可以有傳回值,但不能接受任何參數,否則將會編譯出錯。當我們需要從外部擷取輸入時,可以藉助閉包或者調用其他函數。

代碼 11

      接著,在create_task函數返回的task對象上調用then函數建立一個continuation,如代碼12所示。這個continuation會在前一個task結束之後才開始,從而確保執行第二、三步所需的資料在執行之前準備好。

代碼 12

      最後,在then函數返回的task對象上調用then函數建立一個continuation,執行第四步,如代碼13所示。理論上,你可以通過then函數建立任意數目的continuation。值得提醒的是,在Metro風格的應用程式裡,continuation預設是在UI線程裡執行的,因此可以在continuation裡直接更新UI控制項而不必使用Dispatcher對象,但是,如果你想在後台執行continuation,你需要把task_continuation_context::use_arbitrary傳給then函數的_ContinuationContext參數。

代碼 13

      如果你把這些程式碼群組合起來放在main函數裡執行,並且在最後放置一個cin.get()等待結果,那麼一切都會運作正常。但是,如果你把它們放在一個work函數裡,然後在main函數裡調用這個work函數,你可能會碰到異常,大概是說我們讀了不該讀的地方。這是因為我們的task是非同步執行的,執行的過程中work函數可能已經返回了,連帶那些分配在棧上的變數也一併銷毀了,如果此時訪問那些變數就會出錯。怎麼解決這個問題?

      前面曾經說過,我們傳給create_task函數的Lambda可以有傳回值,這個傳回值將會通過參數傳給後續的continuation,我們可以通過這個機制把那些變數內化到Lambda裡,如代碼14所示。

代碼 14

      值得提醒的是,我們通過tuple對象把第二、三步的計算結果傳給第四步,然後通過tie函數把tuple對象裡的資料提取到兩個變數裡,這種寫法類似於F#的"let sum_of_odds, first_prime = operands"。

      另外,如果你擔心在task之間傳遞vector<int>會帶來效能問題,可以通過智能指標單獨處理,如代碼15所示。智能指標本身是一個對象,會隨著work函數的返回而銷毀,因此需要通過按值傳遞的方式捕獲它。

代碼 15

      到目前為止,我們還沒有任何異常處理的代碼,如果其中一個task拋出異常怎麼處理?我們可以在任務鏈的末端加上一個特殊的continuation,如代碼16所示,它的參數是一個task對象,任務鏈上的任何一個task拋出來的異常都會傳到這裡,這個異常可以通過調用get函數重新拋出,因此我們用一個try…catch語句把get成員函數的調用包圍起來,然後處理它拋出來的異常。

代碼 16

 

你可能會問的問題

      1. 使用PPL需要什麼條件?

      parellel_for、parellel_for_each和parallel_invoke等函數可以在Visual Studio 2010上使用,使用時需要包含ppl.h標頭檔並引用Concurrency命名空間,而parellel_transform和parallel_reduce函數,以及和task相關的部分則需要Visual Studio 2012,使用時需要分別包含ppl.h和ppltask.h標頭檔。

      2. 能否推薦一些PPL的參考資料?

      關於本文提到的PPL函數和類型,可以參考MSDN的concurrency類庫。另外,MSDN的Parallel Patterns Library (PPL)和Parallel Programming with Microsoft Visual C++: Design Patterns for Decomposition and Coordination on Multicore Architectures也是很好的學習資料。

      3. STL是否提供task的替代品?

      C++ 11的STL提供了std::future類,結合std::async函數可以實現task的非同步效果,如代碼17所示,但std::future類目前不支援contiuation,只能通過get成員函數擷取結果,調用get成員函數的時候,如果相關代碼還在執行,則會阻塞當前線程。

代碼 17

      4. PPL能否在Windows以外的平台上使用?

      PPL目前只能在Windows上使用,如果你想在其他平台上進行類似的並行編程,可以考慮Intel Threading Building Blocks,它同時支援Windows、Mac OS X和Linux,提供的API和PPL的類似。TBB是開源的,Intel為它供應商業和GPLv2兩種許可協議。

      5. 能否推薦一些TBB的參考資料?

      Intel Threading Building Blocks: Outfitting C++ for Multi-Core Processor Parallelism是一本不錯的學習資料,另外,Intel也提供了豐富的範例程式碼。

 

*聲明:本文已經首發於InfoQ中文站,著作權,《遇見PPL:C++ 的並行和非同步》,如需轉載,請務必附帶本聲明,謝謝。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.