◆張文培 宋玉貴 潘錦莉
?
簡(jiǎn)析C#多核并行編程
◆張文培 宋玉貴 潘錦莉
(西安工業(yè)大學(xué) 陜西 710000)
隨著處理器多核化的不斷發(fā)展,計(jì)算機(jī)的硬件計(jì)算能力在不斷加強(qiáng),軟件要充分利用計(jì)算機(jī)硬件技術(shù)帶來(lái)的好處,這就促成了并行計(jì)算成為軟件編程的主流。C#支持并行編碼,并且.NET Framework 4.0的并行擴(kuò)展將并行編程的構(gòu)造單元從線(xiàn)程提升到任務(wù),極大程度地降低了編程難度,提升了編程效率。
多核;并行編程;任務(wù);.NET
多核處理器的出現(xiàn)使得傳統(tǒng)的串行編程模式無(wú)法利用多核、多處理器的優(yōu)勢(shì),隨著多核、多處理器平臺(tái)的出現(xiàn),多核編程也得到了更深層次的發(fā)展,而現(xiàn)在并行編程技術(shù)的發(fā)展并不能完全利用多核的優(yōu)勢(shì),因而尋求新的并行編程技術(shù)是十分必要的[1]。
C#是.NET平臺(tái)中引入完全面向?qū)ο蟮木幊陶Z(yǔ)言,是C/C++的衍生語(yǔ)言,不但將C/C++的強(qiáng)大功能繼承下來(lái),并且去掉了它們的一些復(fù)雜特性,是一種能快速高效地實(shí)現(xiàn)基于.NET平臺(tái)軟件開(kāi)發(fā)的編程語(yǔ)言,且C#已經(jīng)支持并發(fā)代碼,這對(duì)于實(shí)現(xiàn)C#多核并行編程是十分必要且有利的。
1.1 多核環(huán)境
隨著社會(huì)數(shù)字化、信息化的不斷發(fā)展,對(duì)于計(jì)算機(jī)對(duì)數(shù)據(jù)的處理速度要求越來(lái)越快,在現(xiàn)代計(jì)算機(jī)技術(shù)的不斷發(fā)展下,多核處理器已經(jīng)成為計(jì)算機(jī)的處理器的主流,現(xiàn)代計(jì)算機(jī)大多都至少有一個(gè)雙核的微處理器。
多核處理器是將兩個(gè)及其以上的具有完全功能的核心集成在同一個(gè)芯片上,并將整個(gè)芯片作為整體對(duì)外輸出[1]。多核處理器所集成的核心可以是單線(xiàn)程處理核心或多線(xiàn)程處理核心,集成后的多核處理器可同時(shí)執(zhí)行的線(xiàn)程數(shù)或任務(wù)書(shū)是原本單處理器的數(shù)倍,這為處理器實(shí)現(xiàn)更高程度的并行提供了可能。并且,由于所有的核心都集成在片內(nèi),這就縮短了核心之間通信更高效,縮短核間的通信延遲,數(shù)據(jù)傳輸帶寬也得到了提高[2]。除此之外,由于多核處理器的集成性,功耗降低,提高了其片上資源的利用率。由于多核處理器結(jié)構(gòu)簡(jiǎn)單,易于優(yōu)化,擴(kuò)展性強(qiáng),這些特點(diǎn)使得多核的發(fā)展并逐漸取代單處理器成為主流[3]。
1.2. 并行編程
傳統(tǒng)的串行代碼是順序執(zhí)行,且只用一個(gè)處理器,但隨著現(xiàn)在多核的普及,串行程序無(wú)法充分利用多核的特點(diǎn),這就促進(jìn)了多核并行程序設(shè)計(jì)的出現(xiàn)。并行程序設(shè)計(jì)是指同時(shí)對(duì)多個(gè)任務(wù)、多條指令或多個(gè)數(shù)據(jù)項(xiàng)同時(shí)進(jìn)行處理,是程序設(shè)計(jì)的一種形式,能夠充分利用底層硬件所提供的并行執(zhí)行能力從而提高程序的運(yùn)行效率。
并行程序設(shè)計(jì)的重點(diǎn)就是要找到程序中的并行性,盡可能降低由于并行化引起的開(kāi)銷(xiāo)[4]。有時(shí),并行化并不是優(yōu)化算法的最佳選擇,如果相比于串行執(zhí)行的方式,并行化能夠帶來(lái)顯著地性能提升,那么并行化才有意義。判斷是否適合并行化并沒(méi)有一勞永逸的方法——一切都取決于特定問(wèn)題的功能需求和性能需求。在對(duì)現(xiàn)有程序進(jìn)行并行優(yōu)化的時(shí)候,必須理解現(xiàn)有的串行設(shè)計(jì),或者理解提供了有限可擴(kuò)展性的并行化算法,然后再對(duì)現(xiàn)有設(shè)計(jì)進(jìn)行重構(gòu),從而使其獲得性能提升,而且不會(huì)引入問(wèn)題或產(chǎn)生不同的結(jié)果。在衡量程序并行化的效果時(shí),可以用加速比來(lái)比較并行效果。其中,是加速比,是串行運(yùn)算時(shí)間,也可表示為單處理器運(yùn)行時(shí)間;是在有個(gè)核的處理器上的運(yùn)行時(shí)間。
1.3 NET Framework 4
在傳統(tǒng)的順序式代碼中,指令一條接著一條運(yùn)行,這種方式并不能發(fā)揮多內(nèi)核的優(yōu)勢(shì),因?yàn)轫樞蛑噶钪荒苓\(yùn)行在一個(gè)可用內(nèi)核上。因此,在多核環(huán)境下,軟件設(shè)計(jì)和程序編寫(xiě)能夠準(zhǔn)備好充分發(fā)揮多核系統(tǒng)的功能是一件十分重要的事情,但使用Visual C# 2010編寫(xiě)的順序代碼并不能發(fā)揮多核的優(yōu)勢(shì),除非利用.NET Framework4所提供的新功能將任務(wù)分解到多個(gè)內(nèi)核上,Visual C# 2010和.NET Framework 4使得將基于任務(wù)的設(shè)計(jì)轉(zhuǎn)換為并行化代碼變得非常簡(jiǎn)單。
NET Framework 4.0可實(shí)現(xiàn)創(chuàng)建并行代碼新模型,這個(gè)新模型稱(chēng)為輕量級(jí)并發(fā),這個(gè)模型減少了在不同邏輯內(nèi)核上創(chuàng)建和執(zhí)行代碼所需要的總開(kāi)銷(xiāo)。這并不是說(shuō)能夠完全消除并行化帶來(lái)的開(kāi)銷(xiāo),但是這個(gè)模型本身是為現(xiàn)代多核微處理器而設(shè)計(jì)的。重量級(jí)并發(fā)模型是在多處理器的時(shí)代出現(xiàn)的,在那個(gè)時(shí)代計(jì)算機(jī)可能有很多物理微處理器,每個(gè)微處理器只有一個(gè)內(nèi)核。輕量級(jí)的并發(fā)模型考慮了新的微架構(gòu),這個(gè)架構(gòu)中有很多由一些物理內(nèi)核支撐的邏輯內(nèi)核。輕量級(jí)并發(fā)模型并不只是關(guān)注不同邏輯內(nèi)核之間的作業(yè)調(diào)度,它還在框架級(jí)別添加了對(duì)多線(xiàn)程訪(fǎng)問(wèn)的支持,從而使得代碼更容易理解。為了應(yīng)對(duì)多核和眾核的負(fù)載型,.NET Framework引入了新的Task Parallel Library(任務(wù)并行庫(kù),TPL),TPL是在多核時(shí)代應(yīng)運(yùn)而生的,TPL提供了一個(gè)輕量級(jí)的框架,可以讓開(kāi)發(fā)人員可以應(yīng)付各種不同的并行場(chǎng)合。
TPL引入了一個(gè)新的命名空間System.Threading.Task.通過(guò)這個(gè)命名空間可以引用.NET Framework 4中并行編程所需要的類(lèi)、結(jié)構(gòu)和枚舉[5]。
TPL支持的數(shù)據(jù)場(chǎng)合主要包括以下三種。
(1)數(shù)據(jù)并行(data parallelism)
需要有大量數(shù)據(jù)需要處理,而且必須對(duì)每一份數(shù)據(jù)執(zhí)行相同的操作。
(2)任務(wù)并行(task parallelism)
有很多可以并發(fā)運(yùn)行的不同操作,通過(guò)任務(wù)并行發(fā)揮并行化的優(yōu)勢(shì)。
(3)流水線(xiàn)(pipelining)
是任務(wù)并行和數(shù)據(jù)并行的結(jié)合體。
2.1 Parallel
C#實(shí)現(xiàn)并行編程最簡(jiǎn)單、最基礎(chǔ)的方法就是利用Parallel類(lèi),在編寫(xiě)并行代碼的時(shí)候通過(guò)使用Parallel靜態(tài)類(lèi)(System.Threading.Tasks.Parallel)所提供的方法實(shí)現(xiàn)并行循環(huán)。
2.1.1 Parallel. Invoke、Parallel.For、Parallel.ForEach
在Parallel下面有三個(gè)常用的方法Invoke,F(xiàn)or和ForEach。在需要使用的時(shí)候,引入命名空間System.Threading.Tasks,即可直接編輯Parallel.Invoke、Parallel.For、Parallel.ForEach指令。
Parallel.For——為固定數(shù)目的獨(dú)立For循環(huán)迭代提供了負(fù)載均衡的潛在并行執(zhí)行。
Parallel.ForEach——為固定數(shù)目的獨(dú)立For Each循環(huán)迭代提供了負(fù)載均衡的潛在的并行執(zhí)行。這個(gè)方法支持自定義的分區(qū)器,可以更好地掌握數(shù)據(jù)分發(fā)。
Parallel.Invoke——對(duì)給定的獨(dú)立任務(wù)提供潛在的并行執(zhí)行。
2.1.2 Parallel退出循環(huán)和異常處理
(1)退出并行循環(huán)
在串行循環(huán)中,想要退出循環(huán),直接使用break語(yǔ)句即可直接退出。但在并行循環(huán)中,并行循環(huán)參數(shù)提供了一個(gè)ParallelLoopState,可用于終止Parallel.For和Parallel.ForEach。ParallelLoopState的loopState實(shí)例提供了Break和Stop兩種方法。
Break——通知并行計(jì)算盡快退出循環(huán),在完成當(dāng)前迭代后盡快地停止執(zhí)行。
Stop——通知并行計(jì)算立即停止執(zhí)行。
(2)并行循環(huán)的異常處理
在串行循環(huán)中,普通的Exception就可捕獲異常,但由于在并行計(jì)算過(guò)程中,多個(gè)迭代在同時(shí)進(jìn)行,就會(huì)產(chǎn)生N個(gè)異常并行出現(xiàn),傳統(tǒng)的異常管理方式已經(jīng)不能滿(mǎn)足,為并行計(jì)算而產(chǎn)生的新的System.AggregateException即可捕獲到并行計(jì)算中異常。AggregateException包含了一個(gè)或多個(gè)在并行和并發(fā)代碼執(zhí)行過(guò)程中產(chǎn)生的異常[4]。
2.2 Task
處理并行計(jì)算時(shí),通常使用的方式就是多線(xiàn)程,在.NET Framework 4之前使用最多的就是Thread,通過(guò)創(chuàng)建多個(gè)線(xiàn)程或者利用線(xiàn)程池的方法來(lái)實(shí)現(xiàn)并行計(jì)算。但在.NET Framework 4之后,TPL引入了新的基于任務(wù)的編程模型,這種編程模型同樣可以充分發(fā)揮多核的優(yōu)勢(shì),優(yōu)化程序,不需要編寫(xiě)底層、復(fù)雜且重量級(jí)的線(xiàn)程代碼,創(chuàng)建任務(wù)比創(chuàng)建線(xiàn)程具有更小的性能開(kāi)銷(xiāo)。但是任務(wù)和線(xiàn)程之間還是有區(qū)別的。
(1)任務(wù)是架構(gòu)在線(xiàn)程之上的,即任務(wù)運(yùn)行的時(shí)候還是需要線(xiàn)程實(shí)現(xiàn)。
(2)任務(wù)與線(xiàn)程之間并不是一對(duì)一的關(guān)系,一個(gè)線(xiàn)程可以運(yùn)行多個(gè)任務(wù),一個(gè)任務(wù)也可由多個(gè)線(xiàn)程完成。
(3)創(chuàng)建任務(wù)相較于創(chuàng)建線(xiàn)程而言開(kāi)銷(xiāo)更小。
2.2.1 Task的生命周期
Task的初始狀態(tài):
Created:利用構(gòu)造函數(shù)創(chuàng)建Task實(shí)例時(shí)的初始狀態(tài),利用Task.Factory.StartNew創(chuàng)建時(shí)直接跳過(guò)。
WaitingToRun:利用Task.Factory.StartNew進(jìn)行創(chuàng)建時(shí)的初始狀態(tài)。
RanToCompletion:任務(wù)執(zhí)行完畢。
創(chuàng)建Task的方法有兩種:
(1)利用構(gòu)造函數(shù)
vartask1=new Task()(=>{ });
(2)利用Task.Factory.StartNew進(jìn)行創(chuàng)建
Var task2=Task.Factory.StartNew()(=>{ });
2.2.2 Task的任務(wù)控制
Task的特點(diǎn)就在于其任務(wù)控制部分,編程人員可以通過(guò)調(diào)整Task的執(zhí)行順序,從而實(shí)現(xiàn)Task的有序工作,Task提供了以下幾種方法。
(1)Task.Wait()——當(dāng)前線(xiàn)程一直等待任務(wù)執(zhí)行完成后執(zhí)行。
(2)Task.WaitAll()——當(dāng)前線(xiàn)程會(huì)等待Task實(shí)例以Task數(shù)組的形式作為參數(shù)被接受,Task之間通過(guò)逗號(hào)隔開(kāi),這個(gè)方法是同步執(zhí)行的。
(3)Task.ContinueWith()——第一個(gè)Task完成后自動(dòng)啟動(dòng)下一個(gè)Task,實(shí)現(xiàn)Task的延續(xù)。
(4)Task的取消
Task是并行計(jì)算的,在.NET Framework 4.0中提供了一個(gè)取消標(biāo)記(CancellationTokenSource.Token),在創(chuàng)建task的時(shí)候傳入此參數(shù),就可以將主線(xiàn)程和任務(wù)相關(guān)聯(lián),然后在任務(wù)中調(diào)用ThrowIfCancellationRequested方法來(lái)等待主線(xiàn)程使用Cancel來(lái)通知,一旦cancel被調(diào)用,task將會(huì)拋出OperationCanceledException來(lái)中斷此任務(wù)的執(zhí)行,Task實(shí)例會(huì)轉(zhuǎn)入TaskStatus.Canceled狀態(tài),并且IsCanceled屬性才回被設(shè)置為true。
2.2.3 Task的異常處理
當(dāng)很多任務(wù)并行運(yùn)行的時(shí)候,可能會(huì)并行發(fā)生很多異常。Task實(shí)例能夠處理一組一組的異常,這些異常利用System.AggregateException類(lèi)處理。初次之外,通過(guò)Task的屬性也可判斷Task的狀態(tài),例如:IsCompleted,IsFaulted,IsCancelled等。
隨著多核時(shí)代的到來(lái),多核并行編程已然成為并行編程的主流模式,.NET Framework 4.0增加的TPL,很好地滿(mǎn)足了多核處理器的應(yīng)用要求。Parallel類(lèi)和Task類(lèi)的出現(xiàn)使得在編寫(xiě)并行計(jì)算的程序時(shí),不需要考慮底層的運(yùn)行機(jī)制,極大地簡(jiǎn)化了程序的復(fù)雜性,并且相較于之前通過(guò)創(chuàng)建多線(xiàn)程實(shí)現(xiàn)并行編程的方法而言,程序性能開(kāi)銷(xiāo)更小,更靈活。所以,在用C#實(shí)現(xiàn)多核并行編程時(shí),可以更多地通過(guò)TPL的方式實(shí)現(xiàn)。
[1]彭曉明,郭浩然,龐建民.多核處理器——技術(shù)、趨勢(shì)和挑戰(zhàn)[J].計(jì)算機(jī)科學(xué),2012.
[2]曹折波,李青.多核處理器并行編程模型的研究與設(shè)計(jì)[J].計(jì)算機(jī)工程與設(shè)計(jì),2010.
[3]黃國(guó)睿,張平,魏廣博. 多核處理器的關(guān)鍵技術(shù)及其發(fā)展趨勢(shì)[J].計(jì)算機(jī)工程與設(shè)計(jì),2009.
[4]高嵐,王銳,錢(qián)德沛.多核處理器并行程序的確定性重放研究[J].軟件學(xué)報(bào),2013.
[5]Hillar G. Professional Parallel Programming with C#:Master Parallel Extensions with. NET 4[M].Wrox Press Ltd. 2010.