19.3 讓一切“并行”——任務并行庫原理及應用
19.3.1 任務并行庫簡介
任務并行庫( TPL : Task Parallel Library )是 .NET 4.0 為幫助軟件工程師開發(fā)并行程序而提供的一組類,位于 System.Threading 和 System.Threading.Tasks 這兩個命名空間中,駐留在 3 個 .NET 核心程序集 mscorlib.dll 、 System.dll 和 System.Core.dll 里。使用這些類,可以讓軟件工程師在開發(fā)并行程序時,將精力更關注于問題本身,而不是諸如線程的創(chuàng)建、取消和同步等繁瑣的技術細節(jié)。
使用 TPL 開發(fā)并行程序,考慮的著眼點是“任務 (task) ”而非“線程”。
一個任務是一個 Task 類的實例,它代表某個需要計算機執(zhí)行的數據處理工作,其特殊之處在于:
在 TPL 中,任務通常代表一個可以被計算機 并行執(zhí)行 的工作。
任務可以由任何一個線程執(zhí)行,特定的任務與特定的線程之間沒有綁定關系。在目前的版本中, TPL 使用 .NET 線程池中的線程來執(zhí)行任務。
負責將任務“分派”到線程的工作則“任務調度器( Task Scheduler )”負責。任務調度器集成于線程池中。
我們在前面介紹并行計算基本原理時,曾經介紹過 OpenMP ,通過在 Fortran 或 C/C++ 代碼中添加特定的編譯標記,實現(xiàn)了 OpenMP 標準的編譯器會自動地生成相應的并行代碼。然而, TPL 采用了另一種實現(xiàn)方式,它自行是作為 .NET 平臺的一個有機組成部分而出現(xiàn)的,并不對編譯器提出特殊要求,當應用程序使用 TPL 編寫并行程序時,所有代碼會被直接編譯為 IL 指令,然后由 CLR 負責執(zhí)行之,整個過程完全等同于標準的 .NET 應用程序。換言之,對于應用軟件開發(fā)工程師而言,使用 TPL 開發(fā)并行程序,在編程方式上沒有任何變化,只不過是編程時多了幾個類可用,并且處理數據時需要使用并行算法。
提示:
之所以微軟在設計 .NET 4.0 并行擴展的時候放棄了類似于 OpenMP 的方式,是因為 .NET 平臺本身是跨語言的,如果象 OpenMP 那樣,就不得不對所有的 .NET 編程語言設定特定的編譯指令,并且需要修改現(xiàn)有的各種語言編譯器,這無疑是不明智的一個決定。
另外,針對并行程序中令人頭痛的異常處理問題, TPL 提供了一個增強了的 .NET 異常處理機制,并且在 Visual Studio 中集成了相應的調試工具。
擴充閱讀:
使用 Visual Studio 2010 調試并行程序
Visual Studio 2010 對并行程序的調試提供了強大的手段,給程序設計好斷點以后,可以使用 Threads 窗口查看當前程序的所有線程:
在 圖 19 ? 9 中雙擊某行,可以讓指定的線程成為當前“激活”的“被調試”的線程。
另外, Parallel Tasks 窗口展示了當前程序所運行的所有任務:
在 Parallel Stacks 窗口中,則可以直觀地看到每個線程的調用堆棧:
有關 Visual Studio 2010 調試器的使用方法,請查詢 MSDN 。本書不再贅述。
19.3.2 從線程到任務
在對 TPL 有了基本的了解之后,我們以一個實例來介紹如何使用 TPL 開發(fā)并行程序( 圖 19 ? 12 )。
1 示例簡介
示例項目 CalculateVarianceOfPopulation 完成以下任務:
測試一批數據的總體方差。
依據數理統(tǒng)計理論,可以使用以下公式計算方差:
很明顯,要完成計算數據總體方差的任務,必須完成以下的工作:
( 1 )計算出所有數據的平均值,這很簡單,直接求數據的和然后除以數據個數就行了。
( 2 )計算所有數與平均值的差值的平方,然后求和
( 3 )將第( 2 )步求出的各除以數據個數,得到總體方差。
分析一下,在上述 3 個子任務中,第( 2 )步是最有可能并行執(zhí)行的。我們可以將整個數據分成幾組,然后對每組數據并行執(zhí)行處理任務。
下面簡要介紹一下示例程序的技術要點,完整代碼可以在配套光盤上找到。
2 直接使用線程實現(xiàn)并行處理
在示例程序中,測試數據是隨機生成的,放在一個 double 類型的數組中,其大小由常量 DataSize 確定。
示例程序是一個 windows 應用程序,為了保證程序可以及時地響應用戶操作,均采用多線程方式在后臺執(zhí)行計算任務,為此設計了一個跨線程安全顯示信息的函數:
private void ShowInfo(string Info)
{
if ( InvokeRequired )
{
Action<string> del = (str) => { rtfInfo.AppendText(str); };
this.BeginInvoke(del, Info);
}
else
rtfInfo.AppendText(Info);
}
注意上面用到了 Control.InvokeRequired 屬性用于判斷是否跨線程訪問 RichTextBox 控件。
串行程序沒什么好說的,示例程序將其封裝為一個 CalculateVarianceInSequence() 函數,直接調用就行了。
有趣的是如何使用線程來并行處理。常量 ThreadCount 用于定義并行執(zhí)行上述第( 2 )個任務的線程數,示例中將其設置為 4 ,因此,在程序運行時,有 4 個線程同時計算“每個數據與總體平均值的差值的平方和”。這是一個典型的線程同步問題。
我們使用一個窗體的成員變量 SquareSumUsedByThread 保存計算結果,由于有 4 個線程要訪問它,因此必須給其加上一把鎖。這里有一個需要注意的地方,為了提升程序性能,這把“鎖”鎖定的對象不能是主窗體對象,更不能是主窗體類型,而是一個專用于互斥的對象。為此,在主窗體中我添加了以下變量:
private object SquareSumLockObject = new object();
而在線程函數中這樣訪問它:
//…
lock (SquareSumLockObject)
{
SquareSumUsedByThread += sum;
}
//…
這是一個很重要的多線程開發(fā)技巧,讀者需要注意。
另外,工作線程在執(zhí)行計算任務時需要知道一些信息:
l 它負責處理整個數組中“哪塊”區(qū)域?這可以通過它要處理的數據的起始索引和要處理的數據個數確定。
l 總體數據的平均值,這個值在算法前一步使用串行算法計算得到的。
讀者一看到這,應該馬上意識到這是一個典型的“將數據從外界傳送到線程中”問題,可以使用本書第 16 章介紹過的相關編程技巧來解決。在本示例中,定義了一個 ThreadArgu 輔助類用于封裝這些信息。由此得到線程函數的代碼框架:
private void CalculateSquareSumInParallelWithThread( object ThreadArguObject )
{
ThreadArgu argu = ThreadArguObject as ThreadArgu ;
// ……(代碼略)
}
另外,由于有 4 個工作線程執(zhí)行計算任務,因此,我們可以使用第 17 章介紹過的 CountdownEvent 對象來等待這 4 個線程的工作結束。
如果讀者掌握了前幾章的內容,那么在上面介紹的基礎之上,您完全可以不看示例代碼自行編出這個程序,這是一個很好的編程練習。
====================================
更多文章、技術交流、商務合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
