.NET4.0并行計(jì)算技術(shù)基礎(chǔ)(3)
這是一個(gè)系列講座,前面兩講的鏈接為:
在前面兩講中,基本上介紹完了并行計(jì)算基礎(chǔ)理論與相關(guān)概念,學(xué)習(xí)不是目的,應(yīng)用才是。因此,本講將介紹一個(gè)并行計(jì)算的例子,并對(duì).NET 4.0的并行擴(kuò)展作一個(gè)總體的介紹。
======================================================
19.1.3 并行計(jì)算所帶來的挑戰(zhàn)
與串行執(zhí)行的程序相比,開發(fā)并行程序需要軟件工程師具備一個(gè)“多線程”的大腦。我們先來看一個(gè)引例,初步體會(huì)一下如何使用
.NET 4.0
所提供的任務(wù)并行庫設(shè)計(jì)并行程序。
1 并行計(jì)算引例
請(qǐng)讀者仔細(xì)查看一下本節(jié)示例程序
SequentialvsParalled
的源碼。此程序完成了一個(gè)非常典型的數(shù)據(jù)處理工作:遞增一個(gè)整數(shù)數(shù)組的每個(gè)元素值。
示例程序?qū)?shù)組大小設(shè)定為
1000000
,然后對(duì)數(shù)組中的每個(gè)元素進(jìn)行
100
次操作,每次操作都將元素值加
1
,因此,完成整個(gè)數(shù)據(jù)處理工作需要
108
次操作。
以下是串行代碼:
//
依次給一個(gè)數(shù)組中指定部分的元素執(zhí)行
OperationCounterPerDataItem
次操作
static void IncreaseNumberInSquence(int[] arr,int startIndex,int counter)
{
for (int i = 0; i <counter; i++)
for (int j = 0; j < OperationCounterPerDataItem; j++)
arr[startIndex+i]++;
}
上述代碼在筆者的雙核筆記本電腦上執(zhí)行時(shí)花費(fèi)了
776
毫秒。
現(xiàn)在,使用
.NET 4.0
所提供的任務(wù)并行庫讓上述操作并行執(zhí)行:
//
將任務(wù)劃分為
TaskCount
個(gè)子任務(wù),然后并行執(zhí)行
static void IncreaseNumberInParallel(int[] arr)
{
int counter = DataSize / TaskCount;
Parallel.For(0, TaskCount, i =>
{
int startIndex = i * counter;
IncreaseNumberInSquence(arr, startIndex, counter);
}
);
}
測試結(jié)果為
419
毫秒,并行加速系數(shù)約為
1.85
。
再改算法,將對(duì)每個(gè)元素的每個(gè)操作設(shè)定為一個(gè)任務(wù),然后再并行執(zhí)行:
static void IncreaseNumberInParallel2(int[] arr)
{
//
為每個(gè)數(shù)據(jù)項(xiàng)創(chuàng)建一個(gè)任務(wù)
Parallel.For(0, arr.Length, i =>
{
Parallel.For(0, OperationCounterPerDataItem, j => arr[i]++);
}
);
}
測試結(jié)果為
10057
毫秒,并行加速系數(shù)為
0.08
,比串行算法慢多了!
2 并行計(jì)算帶來的復(fù)雜性
上面所介紹的例子非常清晰地展示出并行程序設(shè)計(jì)的特殊性,并不是“并行”總比“串行”快的,到底怎樣才能獲得最大的并行加速系數(shù),需要仔細(xì)地設(shè)計(jì)并行算法,并且應(yīng)該在多個(gè)典型的軟硬件環(huán)境中進(jìn)行對(duì)比測試,最終才能得到理想的并行設(shè)計(jì)方案。
開發(fā)并行程序的關(guān)鍵在于要找到一個(gè)合適的任務(wù)分解方案,并行總要付出一定的代價(jià),比如線程同步、線程通訊、同步緩沖數(shù)據(jù)等都是開發(fā)并行程序必須認(rèn)真考慮的問題。
下表對(duì)比了并行程序與串行程序的主要差別:
項(xiàng)目
|
串行程序
|
并行程序
|
程序行為特性
|
可以預(yù)期的,相同運(yùn)行環(huán)境下總可以得到相同的結(jié)果
|
如果沒有提供特定的同步手段,則程序執(zhí)行的結(jié)果無法預(yù)期
|
內(nèi)存訪問
|
獨(dú)占訪問內(nèi)存單元,數(shù)據(jù)可靠
|
有可能因多線程同時(shí)存取同一內(nèi)存單元而引發(fā)數(shù)據(jù)存取錯(cuò)誤
|
鎖
|
不需要
|
必須為共享資源加鎖
|
死鎖
|
不可能出現(xiàn)
|
可能出現(xiàn),需要仔細(xì)考慮程序中可能出現(xiàn)的種種情況予以避免
|
測試
|
使用代碼覆蓋的測試方法可以檢測出絕大多數(shù)
BUG
|
由于多個(gè)線程同時(shí)并行,僅使用代碼覆蓋的測試方法無法檢測出程序中隱藏的
BUG
,并行程序的測試變得很復(fù)雜
|
調(diào)試
|
相對(duì)簡單,可以隨時(shí)停止程序運(yùn)行,單步跟蹤定位到每條語句和每個(gè)變量的值
|
由于多個(gè)線程同時(shí)運(yùn)行,當(dāng)你暫停一個(gè)線程進(jìn)行調(diào)試時(shí),其他線程可能還在運(yùn)行中,因此無法保證調(diào)試環(huán)境的一致性,并行程序的調(diào)試非常困難。
|
正因?yàn)椴⑿谐绦蜷_發(fā)、測試和調(diào)試都比串行程序要困難,所以一般都是先編寫程序的串行版本,等其工作正常之后再將其升級(jí)替換為并行版本。
3 何時(shí)使用“并行計(jì)算”?
根據(jù)前面的介紹,讀者一定對(duì)“并行計(jì)算”有了個(gè)總體的認(rèn)識(shí),由于“并行”需要付出代價(jià),因此,不是所有的程序都需要轉(zhuǎn)換為并行的,當(dāng)要處理的數(shù)據(jù)量很大,或者要執(zhí)行的數(shù)據(jù)處理任務(wù)繁重,并且這些任務(wù)本身就可以分解為互不相關(guān)的子任務(wù)時(shí),使用并行計(jì)算是合適的。
對(duì)于哪些規(guī)模較小的數(shù)據(jù)處理任務(wù),比如你要編寫一個(gè)“通訊簿”小程序來保存和檢索好友信息,就不必考慮并行處理了,因?yàn)橐幚頂?shù)據(jù)量不會(huì)很大,串行算法的性能就可以滿足需求,還用“并行處理”就顯得是“牛刀殺雞”。除了增加程序開發(fā)難度之外沒有什么好處。
19.2 .NET 4.0 中的并行計(jì)算組件
由于并行計(jì)算是將一個(gè)工作任務(wù)進(jìn)行分解以并發(fā)執(zhí)行,因此,任何一個(gè)支持并行計(jì)算的軟件開發(fā)與運(yùn)行平臺(tái)都必須解決這些并發(fā)執(zhí)行的子任務(wù)之間的相互協(xié)作問題,比如:
l
一個(gè)子任務(wù)需要等待其它子任務(wù)的完成,多個(gè)子任務(wù)完成之后才允許執(zhí)行下一個(gè)子任務(wù)(即所謂
fork-join
),
l
一個(gè)子任務(wù)結(jié)束后自動(dòng)啟動(dòng)多個(gè)下級(jí)子任務(wù)的執(zhí)行
l
允許一個(gè)任務(wù)中途取消
l
……
.NET 4.0
通過對(duì)已有的基類庫進(jìn)行擴(kuò)充和增強(qiáng)(
圖
19
?
7
),滿足了上述需求。
如
圖
19
?
7
所示,
.NET 4.0
給
“
System.Threading
”
命名空間增加了一些新的類(,比如在第
17
章介紹過的
Barrier
等幾個(gè)新的線程同步類),同時(shí)對(duì)部分已有類也進(jìn)行了調(diào)整和優(yōu)化。另外,針對(duì)中途取消線程或作務(wù)執(zhí)行這一實(shí)際開發(fā)中非常普遍的需求,提供了一個(gè)統(tǒng)一取消模型(本書第
16
章介紹了此模型)。最大的變化是
.NET
為基類庫提供了多個(gè)與并行計(jì)算密切相關(guān)的類,并將它們統(tǒng)一稱之為“
并行擴(kuò)展(
Parallel Extensions
)
”。
如
圖
19
?
8
所示,
NET 4.0
“并行擴(kuò)展”的主要包括以下幾個(gè)部分:
1
并行語言集成查詢(
PLINQ
,
Parallel Language Integrated Query
)
,這是
.NET 3.0
引入的
LINQ to Object
(本書第
24
章介紹)的換代“產(chǎn)品”,讓查詢操作可以并行執(zhí)行。
2
任務(wù)并行庫(
TPL
,
Task Parallel Library
):
將開發(fā)并行程序的抽象級(jí)別從“線程(
thread
)”提升到“任務(wù)(
Task
)”,只需規(guī)定好計(jì)算機(jī)要執(zhí)行的任務(wù),然后由
.NET
去管理線程的創(chuàng)建和同步等問題。
3
同步的數(shù)據(jù)結(jié)構(gòu)(
CDS
,
Coordination Data Structures
):
包括一組線程安全的常用數(shù)據(jù)結(jié)構(gòu),比如線程安全的隊(duì)列、堆棧等,在并行程序中訪問這些數(shù)據(jù)結(jié)構(gòu),可以不需要顯式地使用
lock
。
4
任務(wù)調(diào)度器(
Task Scheduler
):
負(fù)責(zé)任務(wù)的創(chuàng)建、執(zhí)行、暫停等管理工作。
5
線程池:
.NET 4.0
對(duì)原有的托管線程池功能進(jìn)行了大幅度的增強(qiáng),通過給其集成一個(gè)任務(wù)調(diào)度器,線程池中的線程可以高效地并行執(zhí)行各種任務(wù)。
上述五個(gè)組成部分當(dāng)中,
PLINQ
是建立在
TPL
之上的
,
而
Task Scheduler
是并行計(jì)算的核心,是一個(gè)
Runtime
,它與線程池相集成,負(fù)責(zé)將任務(wù)分派給線程池中的各個(gè)線程執(zhí)行。
下面幾個(gè)小節(jié)中,我們就整個(gè)
.NET 4.0
并行擴(kuò)展中與軟件工程師關(guān)聯(lián)最緊密的兩個(gè)主要組成部分——任務(wù)并行庫
TPL
和并行語言集成查詢
PLINQ
的內(nèi)部機(jī)理進(jìn)行剖析,然后在此基礎(chǔ)上詳細(xì)介紹使用
TPL
和
PLINQ
開發(fā)并行程序的基本技巧。
========================================
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

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