新聞中心
【經(jīng)典譯文】如果你想利用多核機器的強大計算能力,你需要使用PLINQ(并行LINQ),任務(wù)并行庫(Task Parallel Library,TPL)和Visual Studio2010中的新功能創(chuàng)建應(yīng)用程序。

洛川ssl適用于網(wǎng)站、小程序/APP、API接口等需要進行數(shù)據(jù)傳輸應(yīng)用場景,ssl證書未來市場廣闊!成為創(chuàng)新互聯(lián)的ssl證書銷售渠道,可以享受市場價格4-6折優(yōu)惠!如果有意向歡迎電話聯(lián)系或者加微信:18982081108(備注:SSL證書合作)期待與您的合作!
向您推薦:《.NET 4多核并行編程指南》
以前,如果你創(chuàng)建的多線程應(yīng)用程序有BUG,那要跟蹤起來是很麻煩的,但現(xiàn)在情況完全變了,感謝微軟為我們帶來了Microsoft Parallel Extensions for .NET(.NET并行擴展),它在.NET框架線程模型上提供了一個抽象層。
并行擴展遵循微軟在COM應(yīng)用程序中建立的事務(wù)管理和在數(shù)據(jù)訪問領(lǐng)域建立的實體框架和LINQ模型,它試圖通過給.NET框架中的復(fù)雜過程建立高級支持,以便將技術(shù)帶給大眾,隨著多核處理器的普及,開發(fā)人員渴望他們的應(yīng)用程序可以利用所有處理器核心的計算能力。
你可以通過并行LINQ(PLINQ)和任務(wù)并行庫(Task Parallel Library,TPL)使用并行擴展的功能,它們都允許你為單核和多核計算機寫一套代碼,依靠.NET框架,***限度利用代碼執(zhí)行平臺的計算能力,并防止自行創(chuàng)建多線程應(yīng)用程序時常見的陷阱。
PLINQ擴展了LINQ查詢,它將單個查詢分解成多個并行運行的子查詢,TPL允許你創(chuàng)建并行運行的循環(huán),而不是一個接一個地運行,雖然PLINQ的聲明語法使創(chuàng)建并行進程更加簡單,但一般情況下,面向TPL的操作比PLINQ查詢更輕量級。
許多時候,選擇TPL還是PLINQ只是一種生活方式,如果喜歡并行循環(huán),而不是并行查詢,那么設(shè)計一個TPL解決方案比設(shè)計一個PLINQ解決方案更容易。
PLINQ簡介
對于商業(yè)應(yīng)用程序,只要LINQ查詢涉及到多個子查詢時,PLINQ就像金子一樣發(fā)光,如果你要連接本地數(shù)據(jù)庫某張表中的行和另一個遠程數(shù)據(jù)庫某張表中的行,PLINQ將非常有用,在這種情況下,LINQ必須在每個數(shù)據(jù)源上獨立運行子查詢,然后調(diào)和結(jié)果,PLINQ將會把這些子查詢分配給多個處理器核心,這些子查詢就可以同時執(zhí)行。實際上,你使用的處理器周期不是少了,而是更多了,當(dāng)然好處就是你可以更早得到結(jié)果,請閱讀“并行處理不會讓你的應(yīng)用程序變得更快”了解更多關(guān)于多線程應(yīng)用程序的行為。
并行處理不會讓你的應(yīng)用程序變得更快
關(guān)于多線程應(yīng)用程序最常見的一個誤解是,應(yīng)用程序線程越多,運行速度就越快,多啟動一個線程并不會導(dǎo)致Windows給你的應(yīng)用程序更多的處理周期,它只是把這些周期劃分給更多線程了,實際上,在單處理器計算機上,開啟多線程只會讓你的應(yīng)用程序變得更慢。
多線程只是讓你的應(yīng)用程序響應(yīng)更快,但它仍然要等待其它阻塞任務(wù)完成先,不過在等待期間,你可以利用多線程應(yīng)用程序的特點讓其它線程做一些別的事情。在單核機器上,如果線程未被阻塞,多個線程只能相互爭奪有限的處理周期。
多核處理器改變了這種狀況,在多核環(huán)境中,你可以讓W(xué)indows給你的應(yīng)用程序分配更多的處理周期,你不需要阻塞線程,所有線程都在它們自己的核心上執(zhí)行。并行擴展提供了編程結(jié)構(gòu),允許你告訴.NET框架應(yīng)用程序那些部分可以并行執(zhí)行。
即使在多核機器上,PLINQ也并不總是并行的查詢,有兩個原因,一是你的應(yīng)用程序并行運行不會總是更快,第二個原因是,即使你有一個抽象層管理你的線程,在并行處理時總會出現(xiàn)腳步不一致的情況,PLINQ會檢查一些不安全的條件,如果檢測到就不會進行并行查詢。我會指出PLINQ不會檢查的問題和條件,但使用PLINQ出了問題只有你自己負責(zé)處理。
處理PLINQ
調(diào)用PLINQ很簡單,只需要在你的數(shù)據(jù)源中添加AsParallel擴展,下面是一個從本地Northwind數(shù)據(jù)庫連接遠程Northwind數(shù)據(jù)庫,根據(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
因為兩個數(shù)據(jù)源都標(biāo)記了AsParallel(在連接時,如果一個數(shù)據(jù)源使用了AsParallel,另一個也必須使用),因此將會使用PLINQ。
和普通的LINQ查詢一樣,PLINQ查詢使用延遲處理,即等到你要真正使用數(shù)據(jù)時,它才會開始檢索,這意味著即使LINQ查詢聲明了是并行的,在你要處理結(jié)果前不會發(fā)生并行處理,除非使用下面這樣的代碼塊:
- For Each ord As Order In ords
- ord.RequiredDate.Value.AddDays(2)
- Next
在后臺,PLINQ將使用一個線程執(zhí)行For …Each循環(huán)中的代碼,而其它線程可能被用來執(zhí)行子查詢,***可以使用64個線程,請閱讀“并行控制”材料了解這種行為的更多信息。
并行控制
本文認為并行LINQ(PLINQ)總是好的,例如,首先選擇是否要并行運行,然后決定如何將多個子查詢分配給多個線程,你可以使用With*擴展控制PLINQ的行為。
在使用調(diào)試工具的時候,你會發(fā)現(xiàn)PLINQ不是并行執(zhí)行查詢的,你可以傳遞ParallelExecutionMode .ForceParallelism值給WithExecutionMode方法讓其強制并行執(zhí)行查詢。
- ords = From o In le.Orders.AsParallel.
- WithExecutionMode(ParallelExecutionMode.ForceParallelism)
如果你想指定線程的數(shù)量(例如,你想讓一或多個處理核心閑置),你可以使用WithDegreeOfParallelism方法,下面的代碼示例將線程數(shù)限制為3。
- ords = From o In le.Orders.AsParallel.
- WithDegreeOfParallelism(3)
你也可以使用cancellation結(jié)束處理過程,首先創(chuàng)建一個CancellationTokenSource對象,然后將其傳遞給WithCancellation擴展。
- 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án)。
如果在一個訂單(Order)上的處理過程不和另一個訂單上的處理過程共享狀態(tài),可以使用ForAll循環(huán)進一步提高響應(yīng),F(xiàn)orAll可以用于支持Lambda表達式的PLINQ查詢結(jié)果集,它和For…Each循環(huán)不一樣,F(xiàn)or…Each只在程序的主線程中執(zhí)行的,而傳遞給ForAll方法的操作是在PLINQ查詢產(chǎn)生的獨立查詢線程上執(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é)果的順序依賴于各個線程不可預(yù)知的響應(yīng)時間,例如下面這個查詢是為了獲得將要先發(fā)貨的五個訂單。
- 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)度
如果不保證順序,我將獲得一個隨機的訂單(Orders)數(shù)據(jù)集,它們可能是(也可能不是)應(yīng)該先發(fā)貨的五個訂單,為了確保得到前五個訂單,我需要在查詢中增加一個Order By子句,按照日期對查詢結(jié)果進行排序,當(dāng)然這樣就會丟掉PLINQ的一些好處。
因為結(jié)果來自多個線程,難免不會出現(xiàn)異常,PLINQ不能明白“上一條”和“下一條”的概念,如果在你的循環(huán)中剛好要用到下一條項目的值時,完全有可能會遭遇錯誤的處理,為了讓訂單中的項目按照原始數(shù)據(jù)源中的順序處理,你需要在查詢中增加AsOrdered擴展。
例如,如果我想將低于某一運費的所有訂單打包到一起處理,我可能會寫下面這樣一個循環(huán):
- For Each ord As Order In ords
- totFreight += ord.Freight
- If totFreight > FreightChargeLimit Then
- Exit For
- End If
- shipOrders.Add(ord)
- Next
由于并行處理返回的項目順序不可預(yù)知,因此進入批處理的訂單可能是隨機的,為了保證按照原始數(shù)據(jù)源中的順序處理返回的結(jié)果,我必須給數(shù)據(jù)源加上AsOrdered擴展。
- ords = From o In re.Orders.AsParallel.AsOrdered
- Where o.RequiredDate > Now
- Select o
#p#
TPL(任務(wù)并行庫)介紹
如果你的處理不是由LINQ查詢驅(qū)動的,你可以使用借鑒了PLINQ的TPL技術(shù),從根本上看,TPL讓你創(chuàng)建可并行執(zhí)行的循環(huán),如果你的計算機是四核的,一個循環(huán)可能用1/3的時間就完成了。
如果不使用TPL,你可能會像下面這樣處理Orders集合中的所有元素:
- For Each o As Order In le.Orders
- o.RequiredDate.Value.AddDays(2)
- Next
如果使用TPL,你調(diào)用Parallel類的ForEach方法,通過Lambda表達式來處理集合中的項目:
- System.Threading.Tasks.Parallel.ForEach(
- le.Orders, Sub(o)
- o.RequiredDate.Value.AddDays(2)
- End Sub)
通過使用Parallel ForEach,每個方法的實例可以在獨立的處理器上同時處理,如果每個操作需要1毫秒,并且有足夠的處理器存在,所有的訂單就可以在1毫秒內(nèi)處理,而不是1毫秒乘以訂單數(shù)量的時間。
任何復(fù)雜的處理放在Lambda表達式中都會變得很難閱讀,因此你要經(jīng)常想到在你的Lambda表達式中調(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將集合中的成員分配給獨立的任務(wù),這些任務(wù)又被分配到所有處理核心上執(zhí)行,每個任務(wù)完成時釋放掉代碼,TPL調(diào)度器從執(zhí)行隊列中取出另一個任務(wù)開始執(zhí)行,你也可以根據(jù)索引值使用For方法創(chuàng)建一個循環(huán)。
當(dāng)你創(chuàng)建自定義任務(wù)時你才會感覺到TPL的強大之處,任務(wù)創(chuàng)建好后使用它的Start方法啟動,但它更容易使用Task類的靜態(tài)工廠對象(Factory),它的StartNew方法可以創(chuàng)建并啟動任務(wù)(Task),你只需要通過一個Lambda表達式就可以使用StartNew方法,如果你的函數(shù)要返回一個值,你可以使用Task對象的Generic版本指定返回的類型。
下面的示例為計算訂單總價的Order Detail對象創(chuàng)建并啟動了一個Task,Task被添加到一個列表(List)中,后面的代碼循環(huán)檢索List中的結(jié)果,如果我需要一個未計算的結(jié)果,第二個循環(huán)將會暫停,直到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
如果我足夠幸運,在我需要結(jié)果前,Task總是先完成,即使不走運,也要比按順序運行每個Task更早得到結(jié)果。
凡是遇到一個Task的輸出要依賴于另一個Task先完成的情況,你可以在Task之間創(chuàng)建依賴或?qū)ask分組,最簡單的辦法是使用Wait方法,但它會導(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)
一個更復(fù)雜的方法是使用Task對象的ContinueWith方法,當(dāng)其它Task完成時,它觸發(fā)一個Task繼續(xù)運行。下面的例子啟動了多個線程,每個都計算訂單明細(Order Detail)的值,但都只有等到訂單明細上的其它操作完成后才能執(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 并行堆棧窗口提供了一個可視化視圖,顯示了當(dāng)前執(zhí)行的線程的附加信息
#p#
出錯時如何處理
在多個處理器上同時執(zhí)行多個線程也會造成異常出現(xiàn)得更頻繁,任何線程上一旦發(fā)生異常,整個應(yīng)用程序都將掛起,給AggregateException對象添加的錯誤處理也會增加,通過這個對象的InnerExceptions屬性允許你查看每個線程的異常。
- 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
注意這里沒有使用Catch語句,你需要檢查InnerExceptions的類型,確定每個線程究竟拋出的是什么異常。
調(diào)試并發(fā)線程變得更加有趣,因為異??赡茈S一個PLINQ查詢中的循環(huán)出現(xiàn),解決這個問題可能需要重構(gòu)PLINQ查詢,幸運的是,Visual Studio 2010包括了額外的工具調(diào)式并行錯誤。
并行堆棧窗口(Parallel Stacks)超越了舊的線程窗口,線程窗口只能提供一個視圖,而并行堆棧窗口可以顯示所有正在執(zhí)行的線程,例如,它默認允許你同時查看多個線程的調(diào)用堆棧,你可以放大顯示內(nèi)容,也可以過濾只顯示指定的線程,更重要的是,如果你使用TPL,你可以切換到基于任務(wù)的視圖(對應(yīng)于你代碼中的Task對象),或方法視圖(顯示調(diào)用方法的任務(wù)),但使用并行任務(wù)窗口(Parallel Tasks)可能更有用,因為它圍繞Task組織任務(wù),這個窗口不僅顯示當(dāng)前運行的任務(wù),已調(diào)度和等待運行的任務(wù)也會顯示(顯示在狀態(tài)[Status]列),你可以通過檢查當(dāng)前運行的Task是否在等待其它任務(wù),從而確定Task之間的依賴關(guān)系。
在早期的Visual Studio版本中,要一步一步調(diào)式多線程程序是一場噩夢,因為調(diào)試器要從一個線程中的當(dāng)前語句跳轉(zhuǎn)到另一個線程的當(dāng)前語句,并行任務(wù)(Parallel Task)允許你凍結(jié)或解凍與Task相關(guān)的線程,在調(diào)試時控制哪一個線程先運行。
一起使用這兩個窗口可以簡化并行處理問題的診斷,例如,Visual Studio現(xiàn)在檢測到一個死鎖時,它會自動打破死鎖,當(dāng)調(diào)式器檢測到兩個或多個Task不能處理時(因為相互都在等待對方釋放鎖定的對象),Visual Studio將實施凍結(jié)處理,就好像你遇到一個斷點似的,并行任務(wù)窗口將顯示每個Task在等待的對象,以及它占有的線程,并行堆棧窗口的方法視圖可視化顯示了發(fā)生死鎖時哪個Task調(diào)用了哪個方法。
其它調(diào)試功能
除了這些工具外,Visual Studio還包含了其它幾個用于調(diào)式并行處理的功能,在遍歷你的代碼時,當(dāng)你的鼠標(biāo)移到一個Task對象上時,彈出一個提示窗口,顯示該任務(wù)的Id,關(guān)聯(lián)的方法和它當(dāng)前的狀態(tài)(如,等待執(zhí)行)等詳細信息,進一步展開該提示,可以看到該Task的屬性值,包括它的結(jié)果。在觀察窗口(Watch)中檢查Task的InternalCurrent屬性,可以得到當(dāng)前正在執(zhí)行的Task的信息,任務(wù)調(diào)度器(TaskScheduler)的提示展開后可以看到它管理的所有Task。
合理使用PLINQ,TPL和Visual Studio提供的功能,無論你的應(yīng)用程序運行在什么計算機上,你都可以利用所有處理器的計算能力。
網(wǎng)站名稱:探秘.NET4和VisualStudio2010中的多核利用
文章URL:http://www.dlmjj.cn/article/cdiocsi.html


咨詢
建站咨詢
