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

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