探秘.NET 4和Visual Studio 2010中的多核利用
原創(chuàng)【51CTO經(jīng)典譯文】如果你想利用多核機(jī)器的強(qiáng)大計(jì)算能力,你需要使用PLINQ(并行LINQ),任務(wù)并行庫(kù)(Task Parallel Library,TPL)和Visual Studio2010中的新功能創(chuàng)建應(yīng)用程序。
51CTO向您推薦:《.NET 4多核并行編程指南》
以前,如果你創(chuàng)建的多線程應(yīng)用程序有BUG,那要跟蹤起來(lái)是很麻煩的,但現(xiàn)在情況完全變了,感謝微軟為我們帶來(lái)了Microsoft Parallel Extensions for .NET(.NET并行擴(kuò)展),它在.NET框架線程模型上提供了一個(gè)抽象層。
并行擴(kuò)展遵循微軟在COM應(yīng)用程序中建立的事務(wù)管理和在數(shù)據(jù)訪問(wèn)領(lǐng)域建立的實(shí)體框架和LINQ模型,它試圖通過(guò)給.NET框架中的復(fù)雜過(guò)程建立高級(jí)支持,以便將先進(jìn)的技術(shù)帶給大眾,隨著多核處理器的普及,開(kāi)發(fā)人員渴望他們的應(yīng)用程序可以利用所有處理器核心的計(jì)算能力。
你可以通過(guò)并行LINQ(PLINQ)和任務(wù)并行庫(kù)(Task Parallel Library,TPL)使用并行擴(kuò)展的功能,它們都允許你為單核和多核計(jì)算機(jī)寫(xiě)一套代碼,依靠.NET框架,***限度利用代碼執(zhí)行平臺(tái)的計(jì)算能力,并防止自行創(chuàng)建多線程應(yīng)用程序時(shí)常見(jiàn)的陷阱。
PLINQ擴(kuò)展了LINQ查詢,它將單個(gè)查詢分解成多個(gè)并行運(yùn)行的子查詢,TPL允許你創(chuàng)建并行運(yùn)行的循環(huán),而不是一個(gè)接一個(gè)地運(yùn)行,雖然PLINQ的聲明語(yǔ)法使創(chuàng)建并行進(jìn)程更加簡(jiǎn)單,但一般情況下,面向TPL的操作比PLINQ查詢更輕量級(jí)。
許多時(shí)候,選擇TPL還是PLINQ只是一種生活方式,如果喜歡并行循環(huán),而不是并行查詢,那么設(shè)計(jì)一個(gè)TPL解決方案比設(shè)計(jì)一個(gè)PLINQ解決方案更容易。
PLINQ簡(jiǎn)介
對(duì)于商業(yè)應(yīng)用程序,只要LINQ查詢涉及到多個(gè)子查詢時(shí),PLINQ就像金子一樣發(fā)光,如果你要連接本地?cái)?shù)據(jù)庫(kù)某張表中的行和另一個(gè)遠(yuǎn)程數(shù)據(jù)庫(kù)某張表中的行,PLINQ將非常有用,在這種情況下,LINQ必須在每個(gè)數(shù)據(jù)源上獨(dú)立運(yùn)行子查詢,然后調(diào)和結(jié)果,PLINQ將會(huì)把這些子查詢分配給多個(gè)處理器核心,這些子查詢就可以同時(shí)執(zhí)行。實(shí)際上,你使用的處理器周期不是少了,而是更多了,當(dāng)然好處就是你可以更早得到結(jié)果,請(qǐng)閱讀“并行處理不會(huì)讓你的應(yīng)用程序變得更快”了解更多關(guān)于多線程應(yīng)用程序的行為。
并行處理不會(huì)讓你的應(yīng)用程序變得更快
關(guān)于多線程應(yīng)用程序最常見(jiàn)的一個(gè)誤解是,應(yīng)用程序線程越多,運(yùn)行速度就越快,多啟動(dòng)一個(gè)線程并不會(huì)導(dǎo)致Windows給你的應(yīng)用程序更多的處理周期,它只是把這些周期劃分給更多線程了,實(shí)際上,在單處理器計(jì)算機(jī)上,開(kāi)啟多線程只會(huì)讓你的應(yīng)用程序變得更慢。
多線程只是讓你的應(yīng)用程序響應(yīng)更快,但它仍然要等待其它阻塞任務(wù)完成先,不過(guò)在等待期間,你可以利用多線程應(yīng)用程序的特點(diǎn)讓其它線程做一些別的事情。在單核機(jī)器上,如果線程未被阻塞,多個(gè)線程只能相互爭(zhēng)奪有限的處理周期。
多核處理器改變了這種狀況,在多核環(huán)境中,你可以讓W(xué)indows給你的應(yīng)用程序分配更多的處理周期,你不需要阻塞線程,所有線程都在它們自己的核心上執(zhí)行。并行擴(kuò)展提供了編程結(jié)構(gòu),允許你告訴.NET框架應(yīng)用程序那些部分可以并行執(zhí)行。
即使在多核機(jī)器上,PLINQ也并不總是并行的查詢,有兩個(gè)原因,一是你的應(yīng)用程序并行運(yùn)行不會(huì)總是更快,第二個(gè)原因是,即使你有一個(gè)抽象層管理你的線程,在并行處理時(shí)總會(huì)出現(xiàn)腳步不一致的情況,PLINQ會(huì)檢查一些不安全的條件,如果檢測(cè)到就不會(huì)進(jìn)行并行查詢。我會(huì)指出PLINQ不會(huì)檢查的問(wèn)題和條件,但使用PLINQ出了問(wèn)題只有你自己負(fù)責(zé)處理。
處理PLINQ
調(diào)用PLINQ很簡(jiǎn)單,只需要在你的數(shù)據(jù)源中添加AsParallel擴(kuò)展,下面是一個(gè)從本地Northwind數(shù)據(jù)庫(kù)連接遠(yuǎn)程N(yùn)orthwind數(shù)據(jù)庫(kù),根據(jù)客戶(customer)信息查詢訂單(Orders)的示例:
- Dim ords As System.Linq.ParallelQuery(Of ParallelExtensions.Order)
- ords = From c In le.Customers.AsParallel Join o In re.Orders.AsParallel
- On c.CustomerID Equals o.CustomerID
- Where c.CustomerID = "ALFKI"
- Select o
因?yàn)閮蓚€(gè)數(shù)據(jù)源都標(biāo)記了AsParallel(在連接時(shí),如果一個(gè)數(shù)據(jù)源使用了AsParallel,另一個(gè)也必須使用),因此將會(huì)使用PLINQ。
和普通的LINQ查詢一樣,PLINQ查詢使用延遲處理,即等到你要真正使用數(shù)據(jù)時(shí),它才會(huì)開(kāi)始檢索,這意味著即使LINQ查詢聲明了是并行的,在你要處理結(jié)果前不會(huì)發(fā)生并行處理,除非使用下面這樣的代碼塊:
- For Each ord As Order In ords
- ord.RequiredDate.Value.AddDays(2)
- Next
在后臺(tái),PLINQ將使用一個(gè)線程執(zhí)行For …Each循環(huán)中的代碼,而其它線程可能被用來(lái)執(zhí)行子查詢,***可以使用64個(gè)線程,請(qǐng)閱讀“并行控制”材料了解這種行為的更多信息。
并行控制
本文認(rèn)為并行LINQ(PLINQ)總是好的,例如,首先選擇是否要并行運(yùn)行,然后決定如何將多個(gè)子查詢分配給多個(gè)線程,你可以使用With*擴(kuò)展控制PLINQ的行為。
在使用調(diào)試工具的時(shí)候,你會(huì)發(fā)現(xiàn)PLINQ不是并行執(zhí)行查詢的,你可以傳遞ParallelExecutionMode .ForceParallelism值給WithExecutionMode方法讓其強(qiáng)制并行執(zhí)行查詢。
- ords = From o In le.Orders.AsParallel.
- WithExecutionMode(ParallelExecutionMode.ForceParallelism)
如果你想指定線程的數(shù)量(例如,你想讓一或多個(gè)處理核心閑置),你可以使用WithDegreeOfParallelism方法,下面的代碼示例將線程數(shù)限制為3。
- ords = From o In le.Orders.AsParallel.
- WithDegreeOfParallelism(3)
你也可以使用cancellation結(jié)束處理過(guò)程,首先創(chuàng)建一個(gè)CancellationTokenSource對(duì)象,然后將其傳遞給WithCancellation擴(kuò)展。
- Dim ctx As New System.Threading.CancellationTokenSource
- ords = From o In le.Orders.AsParallel.
- WithCancellation(ctx.Token)
- Where o.RequiredDate > Now
- Select o
- For Each ord As Order In ords
- totFreight += ord.Freight
- If totFreight > FreightChargeLimit Then
- ctx.Cancel()
- End If
- Next
如果你正在處理For…Each循環(huán)中的PLINQ查詢結(jié)果,調(diào)用cancellation會(huì)自動(dòng)退出循環(huán)。
如果在一個(gè)訂單(Order)上的處理過(guò)程不和另一個(gè)訂單上的處理過(guò)程共享狀態(tài),可以使用ForAll循環(huán)進(jìn)一步提高響應(yīng),F(xiàn)orAll可以用于支持Lambda表達(dá)式的PLINQ查詢結(jié)果集,它和For…Each循環(huán)不一樣,F(xiàn)or…Each只在程序的主線程中執(zhí)行的,而傳遞給ForAll方法的操作是在PLINQ查詢產(chǎn)生的獨(dú)立查詢線程上執(zhí)行的。
- ords.ForAll(Sub(ord)
- ord.RequiredDate.Value.AddDays(2)
- End Sub)
此外,F(xiàn)or…Each循環(huán)是在它自己的線程中串行執(zhí)行的,而ForAll中的代碼是在檢索訂單的線程上并行執(zhí)行的。
管理順序
雖然和SQL類似,但PLINQ不保證順序,PLINQ子查詢返回結(jié)果的順序依賴于各個(gè)線程不可預(yù)知的響應(yīng)時(shí)間,例如下面這個(gè)查詢是為了獲得將要先發(fā)貨的五個(gè)訂單。
- ords = From o In re.Orders.AsParallel
- Where o.RequiredDate > Now
- Select o
- Take (5)
圖 1 PLINQ給TPL中的功能添加查詢分析和標(biāo)準(zhǔn)查詢操作,TPL提供管理操作系統(tǒng)底層線程需要的基本的結(jié)構(gòu)和調(diào)度
如果不保證順序,我將獲得一個(gè)隨機(jī)的訂單(Orders)數(shù)據(jù)集,它們可能是(也可能不是)應(yīng)該先發(fā)貨的五個(gè)訂單,為了確保得到前五個(gè)訂單,我需要在查詢中增加一個(gè)Order By子句,按照日期對(duì)查詢結(jié)果進(jìn)行排序,當(dāng)然這樣就會(huì)丟掉PLINQ的一些好處。
因?yàn)榻Y(jié)果來(lái)自多個(gè)線程,難免不會(huì)出現(xiàn)異常,PLINQ不能明白“上一條”和“下一條”的概念,如果在你的循環(huán)中剛好要用到下一條項(xiàng)目的值時(shí),完全有可能會(huì)遭遇錯(cuò)誤的處理,為了讓訂單中的項(xiàng)目按照原始數(shù)據(jù)源中的順序處理,你需要在查詢中增加AsOrdered擴(kuò)展。
例如,如果我想將低于某一運(yùn)費(fèi)的所有訂單打包到一起處理,我可能會(huì)寫(xiě)下面這樣一個(gè)循環(huán):
- For Each ord As Order In ords
- totFreight += ord.Freight
- If totFreight > FreightChargeLimit Then
- Exit For
- End If
- shipOrders.Add(ord)
- Next
由于并行處理返回的項(xiàng)目順序不可預(yù)知,因此進(jìn)入批處理的訂單可能是隨機(jī)的,為了保證按照原始數(shù)據(jù)源中的順序處理返回的結(jié)果,我必須給數(shù)據(jù)源加上AsOrdered擴(kuò)展。
- ords = From o In re.Orders.AsParallel.AsOrdered
- Where o.RequiredDate > Now
- Select o
#p#
TPL(任務(wù)并行庫(kù))介紹
如果你的處理不是由LINQ查詢驅(qū)動(dòng)的,你可以使用借鑒了PLINQ的TPL技術(shù),從根本上看,TPL讓你創(chuàng)建可并行執(zhí)行的循環(huán),如果你的計(jì)算機(jī)是四核的,一個(gè)循環(huán)可能用1/3的時(shí)間就完成了。
如果不使用TPL,你可能會(huì)像下面這樣處理Orders集合中的所有元素:
- For Each o As Order In le.Orders
- o.RequiredDate.Value.AddDays(2)
- Next
如果使用TPL,你調(diào)用Parallel類的ForEach方法,通過(guò)Lambda表達(dá)式來(lái)處理集合中的項(xiàng)目:
- System.Threading.Tasks.Parallel.ForEach(
- le.Orders, Sub(o)
- o.RequiredDate.Value.AddDays(2)
- End Sub)
通過(guò)使用Parallel ForEach,每個(gè)方法的實(shí)例可以在獨(dú)立的處理器上同時(shí)處理,如果每個(gè)操作需要1毫秒,并且有足夠的處理器存在,所有的訂單就可以在1毫秒內(nèi)處理,而不是1毫秒乘以訂單數(shù)量的時(shí)間。
任何復(fù)雜的處理放在Lambda表達(dá)式中都會(huì)變得很難閱讀,因此你要經(jīng)常想到在你的Lambda表達(dá)式中調(diào)用下面這樣一些方法:
- System.Threading.Tasks.Parallel.ForEach(
- le.Orders, Sub(o)
- ExtendOrders(o)
- End Sub)
- ...
- Sub ExtendOrders(ByVal o As Order)
- o.RequiredDate.Value.AddDays(2)
- End Sub
從本質(zhì)上講,TPL將集合中的成員分配給獨(dú)立的任務(wù),這些任務(wù)又被分配到所有處理核心上執(zhí)行,每個(gè)任務(wù)完成時(shí)釋放掉代碼,TPL調(diào)度器從執(zhí)行隊(duì)列中取出另一個(gè)任務(wù)開(kāi)始執(zhí)行,你也可以根據(jù)索引值使用For方法創(chuàng)建一個(gè)循環(huán)。
當(dāng)你創(chuàng)建自定義任務(wù)時(shí)你才會(huì)感覺(jué)到TPL的強(qiáng)大之處,任務(wù)創(chuàng)建好后使用它的Start方法啟動(dòng),但它更容易使用Task類的靜態(tài)工廠對(duì)象(Factory),它的StartNew方法可以創(chuàng)建并啟動(dòng)任務(wù)(Task),你只需要通過(guò)一個(gè)Lambda表達(dá)式就可以使用StartNew方法,如果你的函數(shù)要返回一個(gè)值,你可以使用Task對(duì)象的Generic版本指定返回的類型。
下面的示例為計(jì)算訂單總價(jià)的Order Detail對(duì)象創(chuàng)建并啟動(dòng)了一個(gè)Task,Task被添加到一個(gè)列表(List)中,后面的代碼循環(huán)檢索List中的結(jié)果,如果我需要一個(gè)未計(jì)算的結(jié)果,第二個(gè)循環(huán)將會(huì)暫停,直到Task完成。
- Dim CalcTask As System.Threading.
- Tasks.Task(Of Decimal)
- Dim CalcTasks As New List(Of System.
- Threading.Tasks.Task(Of Decimal))
- For Each ord As Order_Detail In
- le.Order_Details
- Dim od As Order_Detail = ord
- CalcTask = System.Threading.
- Tasks.Task(Of Decimal).
- Factory.StartNew(Function() CalcValue(od))
- CalcTasks.Add(CalcTask)
- Next
- Dim totResult As Decimal
- For Each ct As System.Threading.Tasks.Task(Of Decimal) In CalcTasks
- totResult += ct.Result
- Next
如果我足夠幸運(yùn),在我需要結(jié)果前,Task總是先完成,即使不走運(yùn),也要比按順序運(yùn)行每個(gè)Task更早得到結(jié)果。
凡是遇到一個(gè)Task的輸出要依賴于另一個(gè)Task先完成的情況,你可以在Task之間創(chuàng)建依賴或?qū)ask分組,最簡(jiǎn)單的辦法是使用Wait方法,但它會(huì)導(dǎo)致你的應(yīng)用程序停止執(zhí)行,直到所有Task全部完成。
- Dim tsks() As System.Threading.Tasks.Task = {
- Task(Of Decimal).Factory.StartNew(Function() CalcValue(le.Order_Details(0))),
- Task(Of Decimal).Factory.StartNew(Function() CalcValue(le.Order_Details(1)))
- }
- System.Threading.Tasks.Task.WaitAll(tsks)
一個(gè)更復(fù)雜的方法是使用Task對(duì)象的ContinueWith方法,當(dāng)其它Task完成時(shí),它觸發(fā)一個(gè)Task繼續(xù)運(yùn)行。下面的例子啟動(dòng)了多個(gè)線程,每個(gè)都計(jì)算訂單明細(xì)(Order Detail)的值,但都只有等到訂單明細(xì)上的其它操作完成后才能執(zhí)行。
- For Each ordd As Order_Detail In le.Order_Details
- Dim od As Order_Detail = ordd
- Dim adjustedDiscount As New Task(Sub() AdjustDiscount(od))
- Dim calcedValue As Task(Of Long) =
- adjustedDiscount.ContinueWith(Of Long)(Function() CalcValue(od))
- adjustedDiscount.Start
- Next
圖 2 并行堆棧窗口提供了一個(gè)可視化視圖,顯示了當(dāng)前執(zhí)行的線程的附加信息
#p#
出錯(cuò)時(shí)如何處理
在多個(gè)處理器上同時(shí)執(zhí)行多個(gè)線程也會(huì)造成異常出現(xiàn)得更頻繁,任何線程上一旦發(fā)生異常,整個(gè)應(yīng)用程序都將掛起,給AggregateException對(duì)象添加的錯(cuò)誤處理也會(huì)增加,通過(guò)這個(gè)對(duì)象的InnerExceptions屬性允許你查看每個(gè)線程的異常。
- Dim Messages As New System.Text.StringBuilder
- Try
- 'PLINQ or TPL processing
- Catch aex As AggregateException
- For Each ex As Exception In aex.InnerExceptions
- Messages.Append(ex.Message & "; ")
- Next
- End Try
注意這里沒(méi)有使用Catch語(yǔ)句,你需要檢查InnerExceptions的類型,確定每個(gè)線程究竟拋出的是什么異常。
調(diào)試并發(fā)線程變得更加有趣,因?yàn)楫惓?赡茈S一個(gè)PLINQ查詢中的循環(huán)出現(xiàn),解決這個(gè)問(wèn)題可能需要重構(gòu)PLINQ查詢,幸運(yùn)的是,Visual Studio 2010包括了額外的工具調(diào)式并行錯(cuò)誤。
并行堆棧窗口(Parallel Stacks)超越了舊的線程窗口,線程窗口只能提供一個(gè)視圖,而并行堆棧窗口可以顯示所有正在執(zhí)行的線程,例如,它默認(rèn)允許你同時(shí)查看多個(gè)線程的調(diào)用堆棧,你可以放大顯示內(nèi)容,也可以過(guò)濾只顯示指定的線程,更重要的是,如果你使用TPL,你可以切換到基于任務(wù)的視圖(對(duì)應(yīng)于你代碼中的Task對(duì)象),或方法視圖(顯示調(diào)用方法的任務(wù)),但使用并行任務(wù)窗口(Parallel Tasks)可能更有用,因?yàn)樗鼑@Task組織任務(wù),這個(gè)窗口不僅顯示當(dāng)前運(yùn)行的任務(wù),已調(diào)度和等待運(yùn)行的任務(wù)也會(huì)顯示(顯示在狀態(tài)[Status]列),你可以通過(guò)檢查當(dāng)前運(yùn)行的Task是否在等待其它任務(wù),從而確定Task之間的依賴關(guān)系。
在早期的Visual Studio版本中,要一步一步調(diào)式多線程程序是一場(chǎng)噩夢(mèng),因?yàn)檎{(diào)試器要從一個(gè)線程中的當(dāng)前語(yǔ)句跳轉(zhuǎn)到另一個(gè)線程的當(dāng)前語(yǔ)句,并行任務(wù)(Parallel Task)允許你凍結(jié)或解凍與Task相關(guān)的線程,在調(diào)試時(shí)控制哪一個(gè)線程先運(yùn)行。
一起使用這兩個(gè)窗口可以簡(jiǎn)化并行處理問(wèn)題的診斷,例如,Visual Studio現(xiàn)在檢測(cè)到一個(gè)死鎖時(shí),它會(huì)自動(dòng)打破死鎖,當(dāng)調(diào)式器檢測(cè)到兩個(gè)或多個(gè)Task不能處理時(shí)(因?yàn)橄嗷ザ荚诘却龑?duì)方釋放鎖定的對(duì)象),Visual Studio將實(shí)施凍結(jié)處理,就好像你遇到一個(gè)斷點(diǎn)似的,并行任務(wù)窗口將顯示每個(gè)Task在等待的對(duì)象,以及它占有的線程,并行堆棧窗口的方法視圖可視化顯示了發(fā)生死鎖時(shí)哪個(gè)Task調(diào)用了哪個(gè)方法。
其它調(diào)試功能
除了這些工具外,Visual Studio還包含了其它幾個(gè)用于調(diào)式并行處理的功能,在遍歷你的代碼時(shí),當(dāng)你的鼠標(biāo)移到一個(gè)Task對(duì)象上時(shí),彈出一個(gè)提示窗口,顯示該任務(wù)的Id,關(guān)聯(lián)的方法和它當(dāng)前的狀態(tài)(如,等待執(zhí)行)等詳細(xì)信息,進(jìn)一步展開(kāi)該提示,可以看到該Task的屬性值,包括它的結(jié)果。在觀察窗口(Watch)中檢查T(mén)ask的InternalCurrent屬性,可以得到當(dāng)前正在執(zhí)行的Task的信息,任務(wù)調(diào)度器(TaskScheduler)的提示展開(kāi)后可以看到它管理的所有Task。
合理使用PLINQ,TPL和Visual Studio提供的功能,無(wú)論你的應(yīng)用程序運(yùn)行在什么計(jì)算機(jī)上,你都可以利用所有處理器的計(jì)算能力。
原文標(biāo)題:Exploit Multi-Core Processors with .NET 4 and Visual Studio 2010
【編輯推薦】