




版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領
文檔簡介
VisualC++并行編程實戰(zhàn)多喝架構下分工與協(xié)作的設計模式目錄\h第1章引言\h1.1潛在并行化的重要意義\h1.2分解、協(xié)調、可擴展性共享\h1.2.1理解任務\h1.2.2協(xié)調任務\h1.2.3可擴展性數(shù)據(jù)共享\h1.2.4設計方法\h1.3選擇正確的設計模式\h1.4關于術語\h1.5并行的局限\h1.6一些建議\h1.7練習題\h1.8更多資源\h第2章并行循環(huán)\h2.1基本用法\h2.1.1并行版的for循環(huán)\h2.1.2parallel_for_each\h2.1.3期望為何\h2.2實例示范\h2.2.1串行版的CreditReview\h2.2.2parallel_for_each版的CreditReview\h2.2.3性能對比\h2.3模式變體\h2.3.1提前退出循環(huán)\h2.3.2異常處理\h2.3.3小型循環(huán)體的特殊處理\h2.3.4并行度控制\h2.4反面模式\h2.4.1隱性循環(huán)體依賴\h2.4.2少量迭代的小循環(huán)體\h2.4.3重復輸入性枚舉\h2.4.4基于協(xié)同性阻塞的交叉調度\h2.5相關模式\h2.6練習題\h2.7補充閱讀\h第3章并行任務\h3.1基本用法\h3.2實例示范\h3.3模式變體\h3.3.1基于協(xié)同性阻塞的任務協(xié)調\h3.3.2取消一個任務組\h3.3.3異常處理\h3.3.4預測性執(zhí)行\(zhòng)h3.4反面模式\h3.4.1閉包中的變量捕獲\h3.4.2計劃外的取消狀態(tài)傳遞\h3.4.3同步化成本\h3.5設計注意事項\h3.5.1任務組調用約定\h3.5.2任務與線程\h3.5.3如何調度任務\h3.5.4結構化任務組及任務處理\h3.5.5輕量級任務\h3.6練習題\h3.7補充閱讀\h第4章并行聚合\h4.1基本用法\h4.2實例示范\h4.3模式變體\h4.3.1基于小型循環(huán)體的考慮\h4.3.2Combinable對象的其他用處\h4.4設計注意事項\h4.5相關模式\h4.6練習題\h4.7補充閱讀\h第5章Future\h5.1基本用法\h5.2實例示范:Adatum金融儀表盤\h5.2.1業(yè)務對象\h5.2.2分析引擎\h5.3模式變體\h5.3.1取消Future對象\h5.3.2消除瓶頸\h5.3.3在運行時修改任務圖\h5.4設計注意事項\h5.4.1分解到future對象中去\h5.4.2函數(shù)式風格\h5.5相關模式\h5.5.1管道模式\h5.5.2Master/Worker模式\h5.5.3動態(tài)任務并行化模式\h5.5.4離散事件模式\h5.6練習題\h第6章動態(tài)任務并行化\h6.1基本用法\h6.2實例示范\h6.3模式變體\h6.3.1非空while循環(huán)體的并行化\h6.3.2在掛起等待環(huán)境中添加任務\h6.4練習題\h6.5補充閱讀\h第7章管道\h7.1消息塊類型概述\h7.2基本用法\h7.3實例示范\h7.3.1串行化的圖形處理\h7.3.2圖形管道\h7.3.3性能特征\h7.4模式變體\h7.4.1異步管道\h7.4.2管道中的取消操作\h7.4.3管道中的異常處理\h7.4.4多生產者作用下的負載平衡\h7.4.5管道與流的關系\h7.5反面模式\h7.5.1在管道各階段之間進行大量的數(shù)據(jù)拷貝\h7.5.2管道階段中的工作量過小\h7.5.3在消息傳遞時忘記使用隔離技術\h7.5.4無限期的等待\h7.5.5無限制的隊列增長\h7.5.6更多信息\h7.6設計注意事項\h7.7關聯(lián)模式\h7.8練習題\h7.9補充閱讀\h附錄A任務調度器與資源管理器\hA.1資源管理器\hA.1.1為什么需要資源管理器\hA.1.2資源管理器的工作方式\hA.1.3動態(tài)資源管理\hA.1.4超額預訂內核\hA.1.5查詢環(huán)境\hA.2任務的種類\hA.2.1輕量級任務\hA.2.2基于PPL創(chuàng)建的任務\hA.3任務調度器\hA.3.1管理任務調度器\hA.3.2調度算法\hA.3.3使用Context類實現(xiàn)和調度器的通信\hA.3.4設置調度策略\hA.4反面模式\hA.4.1多重資源管理器\hA.4.2資源管理的開銷\hA.4.3來自內聯(lián)任務的非計劃性超額預訂\hA.4.4因線程饑餓而導致的死鎖\hA.4.5忽略進程關聯(lián)碼\hA.5引用資料\h附錄B并行應用程序的調試與分析\hB.1并行任務窗口與并行堆棧窗口\hB.2斷點與內存分配\hB.3并發(fā)可視化工具\hB.4可視化模式\hB.4.1超額預訂\hB.4.2鎖的競爭與串行化\hB.4.3負載平衡\hB.5補充閱讀\h附錄C技術總覽\h補充閱讀\h術語表第1章引言在多核體系環(huán)境中,CPU儀表盤上經(jīng)常顯示著一個問題,即計算機上只有一個內核在滿負荷運行,而其他內核則都被閑置了,這使得我們的程序看上去似乎受到了CPU性能的限制,但事實上,這才僅僅利用了多核系統(tǒng)中的一小部分性能而已。那么,這個問題有更好的解決方案嗎?答案是簡單而明確的,那就是并行編程(parallelprogramming)。現(xiàn)如今,我們越來越多地發(fā)現(xiàn),自己曾一直習慣于寫的并且也為所有程序員熟悉的那種串行化代碼(sequentialcode)顯然已經(jīng)無法滿足用戶的性能需求了。為了進一步提升系統(tǒng)中CPU資源的使用率,我們需要將應用程序分解成能各自為戰(zhàn)的執(zhí)行片斷(piece),以便使它們能夠在同一時間內運行。并行編程使我們可以同一時間內調用多個內核資源,這能有效地提升應用程序的運行速度。當然,說起來容易做起來難。畢竟并行編程一直被認為是專家們的領域,同時也常被視為是一個雷區(qū),其中隱藏著各種難以重現(xiàn)的、詭異的軟件缺陷。幾乎每個程序員都會跟你老生常談一個關于神秘bug如何導致并行程序運行結果與期望大相徑庭的故事。也許,那些故事讓你對自己所面臨的困難有了一些清醒的認識。但幸運的是,你不是一個人在戰(zhàn)斗,并行模式庫(ParallelPatternsLibrary,PPL)和異步代理庫(AsynchronousAgentsLibrary)引入了一個全新的并行編程模型,該模型大大地簡化了編寫并行程序的工作。當然,這些簡化的背后隱藏著一系列精致而復雜的算法,它們能夠很好地適應于多核體系(multicorearchitecture)中的動態(tài)分布式計算。此外,VisualStudio2010開發(fā)系統(tǒng)中還內置了一系列的調試器與分析工具,以便支持這個全新的并行編程模型。如果覺得這里的內容太過拖沓,你也可以根據(jù)自己的需要直接參考1.3節(jié)。另外,你還可以求助于久經(jīng)考驗的設計模式(designpattern),在本書中,我們介紹了一些最重要的、也是最常用的并行編程模式,并提供相應的基于PPL編寫的可執(zhí)行代碼示例,對其進行輔助說明。至于這本書的使用問題,我們建議你最好先大致瀏覽一遍接下來的章節(jié)中提到的6個模式,看看你的問題中是否有一些特征與這些模式是相匹配的。如果答案是肯定的,就說明這些模式(包括相應的代碼示例)是值得你去深入地學習、研究的。盡管編寫并行程序確實是出了名的難,但你不是一個人在戰(zhàn)斗。由于大部分并行程序都應該或多或少地遵守這些設計模式,因此,我們通常很容易就能找到一個相匹配的模式來解決特定的問題。如果真的沒有一個用武之地,那很可能是因為我們遇上了一個難度很大的問題,這時候我們或許就需要去尋求專家或者專業(yè)文獻的幫助了。在本書中,所有的樣例代碼都可以在下面的網(wǎng)站下載:/。1.1潛在并行化的重要意義本書中提到的所有模式都旨在幫助我們發(fā)掘出程序中潛在的并行化。所謂的潛在并行性(potentialparallelism),實質上是指,如果應用程序得到了并行硬件的支持,它就能運行得更快;即使硬件不支持并行化,它的性能也理應與其等價的串行程序相差無幾。只要代碼的架構設計正確,運行時環(huán)境就應該能根據(jù)計算機上具體的工作負載做出自動調整。當然,本書中的這些模式也只能對潛在并行化提供一般性的提示,畢竟誰也無法保證程序在任何情況下都能實現(xiàn)并行執(zhí)行。但是,挖掘這種潛在并行化正是PPL編程模型的核心思想所在,值得我們?yōu)榇诉M一步說明。這里說明了程序中的潛在并行化無論在多核還是單核的情況下,都可以讓處理器中的所有內核保持有效負荷。在常見的并行應用程序中,有不少是為特定的硬件而編寫的。以游戲控制平臺為例,游戲程序員可能事先就對程序在運行時可用的硬件資源了如指掌,包括處理器內核數(shù)量和內存架構等細節(jié)。當然,對于硬件平臺的全面掌握也是嵌入式應用程序開發(fā)的基本特征,例如工程進度控制(industrialprocesscontrol)程序就是這樣。不過,這也決定了這些程序的生命周期必須要和它們所依賴的硬件保持一致。\h[1]相反,如果是在桌面工作站或者服務器這樣的通用計算平臺上編寫程序,我們對硬件特性的估測能力就要打些折扣了。畢竟,我們不可能總能知道有多少內核可供使用,也不太可能預先知道會有什么其他軟件將與程序共同運行。不要對應用程序的并行度進行硬編碼(hardcode),因為運行時可用的內核數(shù)量往往是無法預知的。即便應用程序最初的運行環(huán)境是確定的,隨著時間推移,它也是會發(fā)生變化的。在過去,程序員總是假定自己的程序在下一代硬件中會運行得更快。你現(xiàn)在依然可以這樣認為,因為處理器主頻終究還是會越來越快的。不過,對于現(xiàn)在的多核處理器而言,主頻增長的步伐已經(jīng)開始慢了下來,取而代之的是處理器中的內核越來越多。如果想讓我們的應用程序在多核世界中分享到硬件進步所帶來的利益,就必須要對程序設計模型做出相應地調整,以便程序在今后能運行在多核計算機上。同時,我們還應該致力于發(fā)掘程序中潛在的并行計算能力,以便體現(xiàn)出某種“前瞻性”。處理器的發(fā)展趨勢不再是主頻速度越來越快,取而代之的是增加越來越多的內核。最后,我們還有必要為可能的意外情況做一些準備,畢竟,總會有些用戶無法獲得最新的硬件,這時候,我們需要確保并行程序在單核計算機上的性能至少和只用串行代碼編寫的程序相差無幾,換句話說,就是要讓應用程序在單核和多核之間保持性能的可擴展性,讓其擁有更強的硬件適應能力,并保持前瞻性。而這正是發(fā)掘潛在并行化的目的所在。一個設計良好的并行程序在單核情況下的效率應該與相應的串行程序相差無幾。例如,我們將在第2章中介紹并行循環(huán)模式就是一個發(fā)掘潛在并行化的典型示例。對于一個將要迭代100萬次的for循環(huán)來說,如果能確定其中的每個迭代體都是彼此獨立,我們就完全有理由將這些迭代分解為并行計算,把工作分配到各個閑置的處理器內核中。顯然,具體的分配工作取決于可用內核的數(shù)量,多數(shù)情況下,該循環(huán)的速度將與內核數(shù)成正比。\h[1]軟件的生命周期高度依賴于硬件通常被視為軟件設計的一大失敗,這會導致生產力的浪費。——譯者注1.2分解、協(xié)調、可擴展性共享在這本書中,所有的模式都有一些共同的主題。你將會從后面章節(jié)中看到一個并行程序從設計到實現(xiàn)的全過程,主要內容包括以下三個方面:將工作分解成各個獨立單元(即任務)的方法;協(xié)調并行任務運行的途徑;以及共享任務所需數(shù)據(jù)的可擴展性技術。此外,這本書所討論的模式都屬于設計模式,因此,當我們設計并實現(xiàn)算法,或者思考程序的整體結構時,都可以應用它們。盡管這些示例應用程序的規(guī)模都很小,但其中的原則同樣適用于大型應用程序的架構設計。1.2.1理解任務所謂任務(task),實質上是指一系列相互協(xié)作、共同完成某個較大操作的串行化操作序列。當我們設計一個并行程序的結構時,為任務找到一個合適的劃分粒度,以便它能有效地利用硬件資源是一個十分重要的步驟。如果分得太精細,任務的管理成本將會成為程序的主要開銷;而如果分得太粗糙,就可能會導致處理器的一些內核因無法分配到工作而被閑置,令程序失去了一些并行化的機會。一般情況下,任務應該分得盡可能大,確保它們彼此獨立,并且讓所有的內核都保持有效負荷。除了任務粒度之外,任務調度的方式也是一項不得不考慮的因素。要符合以上所有的目標,我們就必須在設計上做出一定權衡,而只有當我們對應用程序的算法和結構有了充分的認知,才能在分解問題過程中將任務劃分得恰到好處。任務是串行化的工作單元,它們應該被分得盡可能大,在彼此獨立的基礎上,數(shù)量也要足以讓所有內核保持有效負荷。為了讓這些設計原則顯得更具體一些,我們來看一個射線追蹤程序實例。在這個應用中,射線追蹤器(raytracer)負責跟蹤一個場景中的每條光線軌跡,并將這些軌跡合成一張軌跡圖。在這個問題中,如果我們使用單個模擬射線本身作為這個并行計算的單位任務,粒度上是恰到好處的。如果任務粒度再小一些,比如將單個模擬器再進行分解,就只會增加程序的開銷。因為模擬器的數(shù)量已經(jīng)足以讓所有內核保持有效負荷。而如果各個任務的持續(xù)時間差距過大,我們就需要劃分出更多的任務來填補它們之間的空隙。記住,任務不等同于線程(thread),任務和線程在程序調度方面有著很大的不同。任務比線程擁有更多的潛在并行化概念。一個新的線程必然會增加應用程序的并發(fā)性,而一個新的任務只是增加了這種可能性,這種可能性只有當程序獲得足夠多的內核資源時才能實現(xiàn)。此外,將目標任務劃分成這些大而少的子任務還有另一個優(yōu)點,即較大的任務通常彼此間具有更多的獨立性,擁有需要共享的局部變量和域的可能性也更小。然而,遺憾的是,在一些依賴于不穩(wěn)定的、大型的對象圖結構(objectgraph)\h[1]的應用程序中,情況或許正好相反,這些程序中往往會存在一些擁有許多公開的類、方法以及屬性的大型對象模型。這種情況下,任務越大,遇到意外的數(shù)據(jù)共享或其他副作用的可能性就越高??偠灾愕淖罱K目標是問題分解成各個獨立的任務,確保它們之間沒有數(shù)據(jù)共享,并且任務的數(shù)量足以讓處理器的所有內核保持有效負荷。此外,當你考慮內核數(shù)量的時候,也應該把下一代硬件中可能擁有更多內核的因素考慮進去。\h[1]這是一種表示對象引用關系的數(shù)據(jù)結構,后面會進一步提及?!g者注1.2.2協(xié)調任務通常多個任務需要同時運行。彼此不存在依賴的任務可以并行運行。當然,也會有一些任務必須等待別的任務完成之后才能開始。任務的執(zhí)行順序和并行化程度取決于應用程序的內在算法。約束來自于如控制流(算法的步驟流程)和數(shù)據(jù)流(有效的輸入輸出)。協(xié)調任務的機制有很多,具體的運用取決于我們所選擇的并行模式。例如第7章中的管道模式是采用消息機制來協(xié)調任務的。而無論你選擇哪一種機制來協(xié)調任務,都必須充分了解任務之間的依賴關系。只有這樣,我們才有可能獲得一個成功的設計。1.2.3可擴展性數(shù)據(jù)共享通常來說,任務之間總免不了要共享數(shù)據(jù)。但問題是,這些任務在并行化運行的過程中很有可能會對同一塊內存區(qū)域執(zhí)行競爭式訪問和更新,使得相關的數(shù)據(jù)變得非常不可靠。這種不在我們計劃內的數(shù)據(jù)競爭所帶來的后果往往是災難性的。要想避免這個問題,解決方案之一就是使用線程同步技術。也許,你對那些通過阻塞線程來實現(xiàn)的同步并發(fā)線程技術并不陌生,比如鎖、原子性對比交換操作、信號量等都屬于這一類技術。這些技術也確實都能有效地實現(xiàn)訪問共享資源。事實上,我們對于數(shù)據(jù)共享的第一反應似乎也依然是加鎖這一類的同步化(synchronization)機制。但是,此類機制的引入必然會降低程序的并行性(parallelism),因為同步化本身就是串行化(serialization)的一種表現(xiàn)形式。而且,頻繁的鎖權爭奪操作很可能會導致任務無暇去做它們應做的事情,所以如果在程序中使用鎖這樣的技術來實現(xiàn)同步化,是非常容易出錯的。幸運的是,有一些數(shù)據(jù)共享技術可以在不降低性能或增加錯誤率的情況下使用。這些技術包括使用不可變的只讀數(shù)據(jù)、用發(fā)送消息的方式取代更新共享變量,以及在算法中引入新的步驟,以便在一些合適的檢查點(checkpoint)上合并一些可變的本地變量??蓴U展性共享技術可能需要我們修改一些現(xiàn)成的算法。可擴展的數(shù)據(jù)共享需要我們對算法進行一些修改。此外,在面向對象設計中,內存中往往會形成一個復雜而高度連通的對象引用圖,這使得傳統(tǒng)的面向對象模式很難適用于可擴展的并行執(zhí)行要求。對此,我們首先想到的方法或許是,在確保多任務共享的情況下,將這些龐大而互相引用的對象圖成員視為可變的共享狀態(tài),并對這些成員進行外層封裝,用鎖等方式來強制執(zhí)行串行化訪問。遺憾的是,這并不是一種可擴展的共享方式。鎖通常會給所有的內核帶來負面的性能影響,它強制內核暫停和通信的動作會消耗一定的時間;它在代碼中引入串行部分又會降低并行化的可能性。而隨著內核數(shù)量的增長,鎖帶來的開銷會使爭議愈加強烈。當面對越來越多任務需要共享同一塊數(shù)據(jù)的需求時,關于鎖的成本控制將會成為程序性能的主要考量。引入同步化機制(比如鎖)會降低應用程序的可擴展性。除了性能問題之外,這種依賴于同步機制的程序還會有各種各樣的問題,其中就包括死鎖(deadlock)。死鎖通常是指在兩個或者更多的任務之間發(fā)生的彼此都在等待對方釋放鎖的狀況。事實上,大部分關于并行編程的噩夢基本上都起源于錯誤地使用了可變共享狀態(tài)(Sharedmutablestate)或者鎖協(xié)議(Lockingprotocol)。盡管如此,如果我們在對象圖(objectgraph)中適量地使用一些同步化元素,對于編寫可擴展的并行程序是有益的。本書也會盡量謹慎地使用同步機制,你在應用中也應該慎重。事實上,你可以把鎖看做并行編程當中的goto語句,盡管它很容易出錯,但在某些特定情況下這是必需的,并且對于編譯器及其庫而言,它可以被視為一種最左派(bestleft)\h[1]的選擇。沒有人會僅僅因為性能因素就將同步機制完全排除在外,除非是正確性需要。畢竟代碼本身的正確性才是最重要的。總之,在設計的過程中有限地引入同步機制也是非常重要的設計原則。不要事后才想起往應用程序里加入同步機制。\h[1]原文如此,讀者可以理解為最激進的做法。作者的具體意圖應該是想告訴我們,盡量在自己的程序中少使用這些同步機制,這樣代碼才具有可讀性,也比較容易維護。然而和goto語句一樣,有時候用這種簡單粗暴的方式也是好處的?!g者注1.2.4設計方法通常情況下,程序員會先對問題所在的問題域進行甄別,然后在據(jù)此對代碼進行并行化處理,以改善性能,這樣一來,下一次再遇到類似瓶頸時就可以重用這個解決方案了。特別是當我們需要對一系列現(xiàn)存的串行程序進行并行化處理時,這種思路非常吸引人。然而,盡管這樣做或許確實能讓程序的性能獲得初步的提升,但正如之前所描述的那樣,這里隱藏著許多陷阱。因此,傳統(tǒng)的分析、優(yōu)化技術或許并不是最好的選擇。而另一個更好的選擇是:在充分理解具體問題或程序的基礎上,發(fā)掘貫穿整個應用程序的潛在并行化需求。這個過程將會引導你去選擇不同的架構或算法,以便更好地拓展程序在并行化方面的潛力。千萬不要草率地界定性能瓶頸并貿然使其并行化,恰恰相反,我們應該通過改變程序的結構來并行化。在通盤考慮數(shù)據(jù)結構和算法之前,不要急著界定問題的瓶頸所在。分解、協(xié)調以及可擴展性共享技術之間是密切相關的,甚至是環(huán)環(huán)相扣的。因此,當我們在為特定的應用程序選擇設計方案的時候,應該進行全方位的思考。請盡量使用設計模式。讀完上面這些描述,你或許會覺得這太過形而上學,泛泛而談了。究竟如何將問題分解成任務?到底該采用哪種協(xié)調技術,有更具體一點的回答嗎?答案是肯定的,這本書中所描述的設計模式就是最好的回答。它們就是解決這些問題真正的捷徑。一旦你領會了這些設計模式幕后的設計動機,就會自然而然地培養(yǎng)出某種直覺,而這種直覺將會幫助你將這些設計模式及其變體應用到自己的程序中。接下來,我們將會在剩下的章節(jié)中詳細地介紹這些模式,以供參考。1.3選擇正確的設計模式根據(jù)下面這張表,你可以為自己選擇合適的設計模式:此外,你也可以先閱讀每章開頭前兩頁的內容,熟悉一下這6個模式的大致情況,并了解一下它們的基本應用場景,然后再更深入地具體研究它們。1.4關于術語在一般情況下,我們會認為“并行”和“并發(fā)”是同義詞,但在這本書中它們是兩個不同的概念。在本書中,并發(fā)(concurrency)在更多時候是一個與多任務和異步輸入輸出(I/O)相關的概念。它實質上是一種執(zhí)行方式,這種執(zhí)行方式能在多線程輪番爭奪時間片的情況下,確保每一個線程最終都能獲得時間片。為了使程序能應對諸如用戶輸入、設備和傳感器等外部中斷,并發(fā)性是必要的。例如,操作系統(tǒng)和游戲天生就是具有并發(fā)性,即便在單核情況下也是如此。而對于并行化(parallelism)而言,則更強調并發(fā)線程可以同時在多個處理器內核上執(zhí)行的能力。并行編程則側重于增加我們所占用的處理器資源,并且確保有多個處理器時不會被經(jīng)常中斷,以此來提升應用程序的性能。而且,并發(fā)和并行想要達到的目標也并不相同。并發(fā)的主要目標是降低各個線程長期不受阻塞占有時間片的可能性,換句話說,并發(fā)就是要避免出現(xiàn)有線程被餓死的情況。通常,并發(fā)在操作上往往是必要的。例如,在一個單核計算機的圖形操作系統(tǒng)中,會有多個窗口需要同時刷新顯示區(qū)域,這時候圖形用戶界面(GUI)就必須支持并發(fā)。相對而言,而并行化則與吞吐量有關,是一種性能的優(yōu)化,而非功能性需求。它的目標是讓處理器的所有內核保持最大的有效負荷。為此,它所使用的調度算法通常就不是具有搶占性的,正如處理隊列(queue)或棧(stack)的算法所做的工作一樣。1.5并行的局限根據(jù)阿姆達爾定律(Amdahl'slaw)\h[1]得出的結論,并行化所能帶來的性能改善受到了應用程序中串行化處理所占分量的限制。是的,這乍看之下幾乎有點兒違反直覺。但你并沒有看錯。根據(jù)阿姆達爾定律的說法,無論我們有多少內核可用,程序所能得到的最大速度增長率也只能符合(1/串行化處理所消耗的時間)函數(shù)關系。圖1-1對此做了說明。圖1-1當一個應用程序中的串行化處理占25%時的阿姆達定律例如,當內核達到11個時,應用程序的運行速度只比完全串行化運行提高了三倍略多一些。即使在更少量內核的情況下,運行速率也不是線性增長的,圖1-2對此做了說明。圖1-2一個擁有25%串行性的程序性能增長與內核數(shù)量增長之間的關系如圖1-2所示,隨著內核數(shù)量(及整個應用程序的速度)的增加,應用程序的串行化部分所耗費時間所占的比例也會隨即增加(注意,串行化處理時間本身的值并沒有受到影響)。這幾乎也說明了一個問題,或許我們應該對該程序能在四核計算機上得到兩倍的速率增長感到滿足。更重要的問題是,如何才能讓應用程序維持這樣的可擴展性,這取決于具體工作本身所擁有的天然串行性(inherentlysequential)時間。阿姆達爾定律給予我們的另一個提示是:或許應用程序中確實有可能會發(fā)現(xiàn)一些符合并行計算的特性,以至于我們想利用它為程序添加一些額外的功能。例如,電腦游戲開發(fā)員可能會發(fā)現(xiàn),如果新型多核平臺的并行硬件的支持,就可以畫出更為精美復雜的圖像,即使游戲的邏輯(人工智能引擎)并不適用于并行化執(zhí)行。但是,對程序整體性能來說,最終發(fā)揮影響的可能還是應用程序中各功能模塊的具體組合方式。而就具體的實踐來說,我們所能得到的速度增長率通常會比阿姆達爾定律所預計的情況還要糟糕一些。因為隨著處理器內核數(shù)的增加。訪問共享內存的成本也會跟著增加。同時,并行算法中包含的協(xié)調性成本,也不是相應串行方案中所必需的。對此,你可以求助于VisualStudio中的并發(fā)可視化工具,借助其中的分析工具來了解自己所采用的并行化方案的最終效率??偠灾驗閼贸绦蚴怯刹⑿谢糠趾鸵恍┍匾拇谢糠止餐M成的,所以,程序整體性能的增長率不太可能會與處理器的內核數(shù)形成某種線性關系,盡管在某些局部,程序確實能獲得近似的線性增長。正因為如此,我們在分析性能需求時就有一個不能忽略的步驟,即我們在程序中發(fā)掘適于并行執(zhí)行的部分,必須建立在充分理解應用程序結構及其算法的基礎之上。\h[1]阿姆達爾定律(Amdahl'slaw):一個計算機科學界的經(jīng)驗法則,因吉恩·阿姆達爾而得名。它代表了處理器并行運算之后效率提升的能力?!g者注1.6一些建議總而言之,我們應該盡可能選擇一些最簡單而有效的方法,這里有一些最基本的建議:·當你選擇并行應用程序庫或者架構時,應該盡可能地使用最高層的抽象邏輯?!ふ埍M可能地充分利用好應用程序服務器自身的并行化;例如Web服務或數(shù)據(jù)庫本身就具備了一定的并行化?!ふ埵褂肁PI來實現(xiàn)并行化封裝。例如并行模式庫。這些程序庫都是由專家所完成的,都經(jīng)過了完備的測試。有了它們的幫助,你可以解決許多常見的并行編程問題?!ぎ斈阍跇嬎家粋€并行化設計的時候,應該盡可能全面地考慮整個應用程序的架構。一般僅僅關注提高一些性能熱點和焦點的性能。這種性能也確實可能得到一些改善,但通常不會獲得最好的結果?!ふ埍M量使用設計模式來解決問題,例如本書中所介紹的設計模式?!ねǔG闆r下,對算法進行重構(例如移除一些數(shù)據(jù)共享需求)是一個更好的選擇,尤其要比你去修改那些原本就為串行化而設計的底層代碼要好得多?!こ欠浅1匾?,否則絕不要在并發(fā)性任務之間共享數(shù)據(jù)。如果非要進行共享數(shù)據(jù),請使用API中所提供的容器,例如共享隊列(sharedqueue)?!こ侨f不得已,否則應該盡量避免使用類似線程和鎖這樣的低層次元素。同時,要盡可能地將應用程序中的抽象級別從線程提升到任務。1.7練習題1)當你為問題選擇分解粒度(即合并成更大的任務還是分解成更多的小任務)時,需要權衡的因素有哪些?2)在內核數(shù)由1到4的增加過程中,如果程序中串行處理所占的時間約為10%,那么該程序能獲得的最大速度增長率是多少?3)并行與并發(fā)的不同之處有哪些?1.8更多資源如果希望對文中所提到的術語有更好的理解,可以參考本書最后的術語表。本書中所提到的設計模式都屬于被業(yè)界用戶廣泛應用于開發(fā)的并行模式。在這些用戶的概念中,這些模式被看做算法或可實現(xiàn)的模式。關于并行模式的分類,我們可以參考Mattson等人的書,還有OurPatternLanguage(OPL)網(wǎng)站,我們盡可能地保證所有的術語都有據(jù)可查,對于無法做到的情況,文中將會給出相應的解釋。對于MicrosoftWindows平臺上的并行性,我們也可以在Duffy的書中找到更詳細的討論?!uffy,Joe.ConcurrentProgrammingonWindows,Addison-Wesley,2008.·Mattson,TimothyG.,BeverlyA.Sanders,andBernaL.Massingill.PatternsforParallelProgramming.Addison-Wesley,2004.·OPL,OurPatternLanguageforParallelProgrammingver2.0,2010./wiki/patterns.第2章并行循環(huán)并行循環(huán)模式一般適用于執(zhí)行一些具有獨立性的迭代操作,例如遍歷某集合(collection)中的元素或者執(zhí)行一些限定次數(shù)的迭代。但前提是這些操作是彼此獨立的,即循環(huán)步驟中不存在對本地內存或磁盤文件的共同寫操作。在語法方面,并行循環(huán)與我們所熟悉的for和for_each并無多大的區(qū)別,只不過它們在多核環(huán)境中往往會完成得更快而已。此外,另一個不同于串行循環(huán)的是,并行循環(huán)的執(zhí)行順序是不確定的,這意味著在并行條件下各步驟的操作通常會同步進行,有時候,甚至兩個步驟的操作順序會與它們在串行循環(huán)中的情況完全相反。這里唯一能保證的是:當循環(huán)完成時其中所有的步驟都會被執(zhí)行到。我們可以輕而易舉地將一個串行循環(huán)(sequentialloop)改為并行循環(huán)(parallelloop)。但是,你可千萬不能就此認為簡單就不會出錯。畢竟,判斷循環(huán)步驟之間是否具有獨立性是一件非常困難的事。這需要在具體的實踐中不斷地學習并總結經(jīng)驗。有時候,如果我們不幸在一個具有某種依賴關系的循環(huán)中誤用了并行循環(huán)模式,很可能就會導致某些不可預測的行為,程序有可能會就此停止響應,有時甚至還會出現(xiàn)那種運行100萬次才出現(xiàn)一次的詭異bug。總而言之,“獨立性”是我們能否使用并行循環(huán)模式的關鍵,而這也是本章接下來要介紹的重點內容。并行循環(huán)模式一般用于處理基于多組數(shù)據(jù)的獨立性操作。同時,這也是數(shù)據(jù)并行化(dataparallelism)技術的一個標準應用。對于并行循環(huán)來說,決定它并行度的通常不是代碼,而是運行時環(huán)境(run-timeenvironment)。也就是說,它取決于運行時環(huán)境能同時調用多少內核來完成這些循環(huán)步驟。然而,無論我們有多少內核可用,循環(huán)都應該能夠始終正確地運行。即使在單核處理器上,只要該循環(huán)的每次迭代操作所執(zhí)行的工作量不是太小,它的性能就理應與相應的串行代碼相差無幾(也許只相差幾個百分點);一旦有了多核處理器,性能就會立即得到改善。畢竟在多數(shù)情況下,性能與內核數(shù)之間還是會存在著一定的比例關系的。2.1基本用法在并行模式庫(PPL)中,我們已經(jīng)為你內置了與for、for_each對應的并行化函數(shù)。其中,parallel_for函數(shù)可以被用來遍歷整數(shù)區(qū)間,而parallel_for_each函數(shù)則可以用于遍歷用戶所提供的數(shù)據(jù)。如果你想讓帶有獨立性迭代的for和for_each循環(huán)在多核計算機上運行得更快,就應該去使用它們的并行版本。2.1.1并行版的for循環(huán)現(xiàn)在,讓我們來看一段標準C++版的for循環(huán)代碼:vector<double>results=……intworkload=……size_tn=results.size();for(size_ti=0;i<n;++i){results[i]=DoWork(i,workLoad);}接下來,為了體現(xiàn)出多核的性能優(yōu)勢,我們改用parallel_for函數(shù)來代替for關鍵字,并用lambda表達式(lambdaexpression)\h[1]來替代原來的循環(huán)體。如果你想使用并行循環(huán),不要忘記循環(huán)各步驟間需要彼此獨立,確保它們之間的通信不以寫共享變量的方式來實現(xiàn)。vector<double>results=……intworkload=……size_tn=results.size();parallel_for(0u,n,[&results,workLoad](size_ti){results[i]=DoWork(i,workLoad);});現(xiàn)在,只要條件允許,parallel_for函數(shù)就能利用多核資源來遍歷上述代碼中的索引區(qū)間。在代碼中,形如[capturedvariables](args){body}的語句實質上是一個lambda表達式。在繼續(xù)深入閱讀之前,你或許應該去復習一下C++中關于lambda表達式的語法內容。另外,parallel_for函數(shù)還有一系列不同的重載(overload)版本。上述示例中所調用的版本聲明如下。template<typename_Index_type,typename_Function>voidparallel_for(_Index_type_First,_Index_type_Last,const_Function&_Func);在該函數(shù)中,前兩個參數(shù)所表示的是該迭代的邊界。第一個參數(shù)是循環(huán)的下界位;第二個參數(shù)則為循環(huán)的出界位,即上界位+1;而第三個參數(shù)則是一個作用于歷次迭代的函數(shù),迭代次數(shù)是該函數(shù)的參數(shù)之一。這意味著該循環(huán)的每次迭代操作都會調用這個函數(shù)。關于parallel_for函數(shù)的另一個重載,我們將會在2.3節(jié)中再介紹。和串行循環(huán)不同的是,parallel_for方法不能保證操作的順序,即對較大索引值的操作可能排在較小索引值之前。例子中類似于parallel_for第三參數(shù)那樣的,形如[capturedvariables](args){body}的表達式,被稱為lambda表達式。該表達式實質是一個能夠捕獲自己所在作用域中變量的函數(shù)對象。當然,_Func參數(shù)并不一定非得是lambda表達式,它也可以是一個函數(shù)指針\h[2](指向一個在別處聲明的函數(shù))。如果你對lambda表達式的語法還不熟悉,可以參考2.7節(jié)中所推薦的文章。相信我,一旦你使用了lambda表達式,就會對它有一種相見恨晚的感覺。\h[1]lambda表達式是C++11標準(有時候也叫C++0x)中的新特性,不熟悉的讀者可暫時將其理解為一個即寫即用的匿名函數(shù)。——譯者注\h[2]此處的_Func在代碼中是個引用(reference)參數(shù),或許會引起讀者的困惑。而實質上作者是指_Function模板參數(shù)也可以推導為一個指針(pointer)類型,與_Func參數(shù)本身是_Function&類型并不矛盾?!g者注2.1.2parallel_for_each下面是一個C++StandardTemplateLibrary(STL)版的for_each串行循環(huán)示例。vector<size_t>inputs=……intworkload=……for_each(inputs.cbegin(),inputs.cend(),[workLoad](size_ti){DoWork(i,workLoad);});同樣,為了發(fā)揮出多核的性能優(yōu)勢,我們用parallel_for_each函數(shù)來替代for_each關鍵字\h[1]。parallel_for_each會在循環(huán)體內處理容器中的每一個元素。vector<size_t>inputs=……intworkload=……parallel_for_each(inputs.cbegin(),inputs.cend(),[workLoad](size_ti){DoWork(i,workLoad);});在語法上,parallel_for_each函數(shù)與std::for_each基本相同。第一個參數(shù)是一個迭代器(iterator),指向操作區(qū)間內的第一個元素;第二個參數(shù)迭代器則指向該區(qū)間內最后一個元素的后一個位置;第三個參數(shù)則是一個作用于該區(qū)間內每一個元素的函數(shù)對象。不要忘記,由于各迭代操作之間必須保持彼此獨立,因此,每一個循環(huán)體只能更新那些傳遞給它的字段實體。由于parallel_for_each方法的執(zhí)行順序是不確定的(這與for_each串行循環(huán)不同),因此這里的輸入?yún)?shù)并非總能按照既定的順序來處理。\h[1]原文如此,而實際上兩者都是函數(shù),并不存在關鍵字(keyword)?!g者注2.1.3期望為何默認情況下,循環(huán)的并行度(即硬件能同時運行多少次迭代)取決于處理器中可用的內核數(shù)量??捎玫膬群嗽蕉嘌h(huán)便運行得越快,這種趨勢可以一直持續(xù)到我們遇上阿姆達定律所預言的那個收益遞減點。從那之后,循環(huán)究竟能運行得多快就取決于該循環(huán)本身的工作內容了(請參閱第1章關于阿姆達定律的討論)。內核的增加能使循環(huán)運行得更快,但這也總會有個上限。為循環(huán)體選擇正確的粒度是很有必要的,因為有太多過小的并行循環(huán)因其工作量過度分解而導致其多核優(yōu)勢被循環(huán)本身的開銷抵消殆盡。在parallel_for或parallel_for_each的執(zhí)行過程中,如果某個迭代操作拋出了異常(exception),該異常將會被上拋至該函數(shù)所調用的線程處進行處理。在2.3節(jié)中,我們將會更詳細地講解并行循環(huán)中的異常處理問題。強健的異常處理機制是并行循環(huán)處理的重要組成部分之一。在用并行循環(huán)替換了串行循環(huán)之后,如果你發(fā)現(xiàn)程序不能按照自己所預期的方式執(zhí)行,那么,最大的可能就是該循環(huán)步驟之間并非是彼此獨立的。下面,我們將會介紹一些常見的依賴性循環(huán)體(dependentloopbody)\h[1],以你供參考:請仔細檢查循環(huán)迭代之間是否存在某種依賴關系。因為到目前為止,對這種依賴性的疏忽是我們在使用并行循環(huán)時最常見錯誤?!蚕碜兞繄?zhí)行寫操作:如果循環(huán)體內對共享變量執(zhí)行了寫操作,那么該循環(huán)體就是具有依賴性的。這在我們需要執(zhí)行某些聚合求值操作時很常見。下面這個示例中的total就是被所有迭代體共同操作的變量。for(inti=1;i<n;i++)total+=data[i];如果真的遇上了類似的情況,建議你參考第4章中的并行聚合模式。當然,共享變量的形式是多樣化的。任何在循環(huán)體外部聲明的變量都有可能是共享變量。例如,如果我們對一些類似于類或數(shù)組(array)這樣的類型對象使用某種共享性引用的話,這些對象中的所有字段、元素都有可能會被隱式地轉換為共享變量;另外,通過引用或指針方式傳遞的函數(shù)參數(shù)也會導致變量共享;同樣,在lambda表達式中使用引用方式捕捉的變量也一樣會如此?!な褂脤ο竽P偷臄?shù)據(jù)訪問器(dataaccessor):如果循環(huán)體是通過數(shù)據(jù)訪問器來處理一個對象的話,我們就有必要了解一下這些訪問器,看看其中是否涉及了某種共享狀態(tài)抑或只是對象自身的狀態(tài)。例如,下面有個名為GetParent的訪問器方法很可能就涉及了某種全局狀態(tài)(globalstate):在用訪問器讀取數(shù)據(jù)的時候,你必須非常謹慎。因為這些大型對象往往會以令人費解的方式共享它們的可變狀態(tài)。for(inti=0;i<n;i++)SomeObject[i].GetParent().Update();這個例子中的循環(huán)迭代體就很有可能不具備獨立性。因為對于該循環(huán)中的每一個i來說,SomeObject[i].GetParent()引用的都有可能是同一個對象實體?!ひ昧朔蔷€程安全的數(shù)據(jù)類型和函數(shù):如果在一個并行循環(huán)中,循環(huán)體所使用的數(shù)據(jù)類型或函數(shù)不是線程安全的話,就表示這些線程中已經(jīng)存在著依賴關系了,因此我們也就可以斷定該循環(huán)體中的操作是不具有獨立性的?!ぱh(huán)執(zhí)行性依賴(Loop-carrieddependence):如果在parallell_for循環(huán)中,循環(huán)體中所執(zhí)行的數(shù)值運算是基于循環(huán)索引變量的,那么出現(xiàn)循環(huán)執(zhí)行性依賴的可能性就非常大了。例如在下面的代碼中,循環(huán)體中同時引用了data[i]和data[i-1],一旦這樣的代碼出現(xiàn)在parallel_for中,我們是無法確保data[i-1]能在處理data[i]之前就被更新完成的?;谘h(huán)索引變量的數(shù)值運算,特別是加減法操作,通常都會導致循環(huán)執(zhí)行性依賴。for(inti=1;i<N;i++)data[i]=data[i]+data[i-1];然而在某些時候,這種包含循環(huán)執(zhí)行性依賴的并行算法也是可行的,但這已經(jīng)超出了本書的討論范圍。對此,我們最好的建議是:要么考慮一下程序中別的并行化機會,要么分析一下你的算法能否匹配某些用于科學計算的高級并行模式,例如并行掃描和并行動態(tài)規(guī)劃之類的。當我們在尋找并行化機會的時候,對應用程序的性能進行詳細分析,將會有助于我們了解程序的運行時間主要消耗在哪些地方。但是,這種分析終究不能代替你理解應用程序的算法和結構,例如,它不能替你判斷循環(huán)體中是否存在依賴關系。不要對代碼的性能分析寄予過高的期望——它不能替代你分析算法,所以我們只能靠自己。\h[1]此處必然是循環(huán)體(loopbody)而非循環(huán)(loop),因為循環(huán)的索引變量本身也是一種共享變量,而且循環(huán)必然要對它執(zhí)行讀寫操作,這種讀寫操作不會影響循環(huán)體的獨立性?!g者注2.2實例示范接下來,我們將通過一個具體的實例向你演示并行循環(huán)模式的運用。Fabrikam航運公司為自己的商業(yè)用戶增設了一項借貸業(yè)務,他們根據(jù)用戶的信用度來鑒別哪些賬戶可能存在風險。并且在每個賬戶內都包含了一份結欠記錄。因為Fabrikam公司從歷史記錄中發(fā)現(xiàn)了一個規(guī)律:在賬戶被判違約之前的數(shù)月內,那些違約客戶的借貸余額都會呈現(xiàn)出穩(wěn)定的增長趨勢。于是,為了鑒別風險賬戶,F(xiàn)abrikam公司使用了統(tǒng)計學中的趨勢分析法,據(jù)此估算出每個賬戶所能達到的最高負債額。如果分析程序預測某用戶的賬戶將在三個月內超出他的負債限額,該賬戶就會被標記下來,并將面臨Fabrikam公司信用分析師的人工審查。在這個應用中,頂層循環(huán)將會遍歷賬戶庫中的所有用戶。循環(huán)體會根據(jù)用戶的負債記錄制定出一條趨勢線,據(jù)此推定其負債范圍,然后用它來比對之前設定的負債限額。一旦發(fā)現(xiàn)該用戶有超額負載的可能性,程序就會為該賬戶分配警告標志。這個應用程序的一個重要特點是,每個用戶的信用度是可以被獨立估算的。這意味著一個用戶的信用度并不依賴于另一個用戶,這些操作是彼此獨立執(zhí)行的。因此,只要用并行循環(huán)替換掉其中的for_each串行循環(huán),我們的信用分析程序就能運行得更快一些。關于該程序的完整源代碼,讀者可以在中的Chapter2\CreditReview目錄下找到。2.2.1串行版的CreditReview下面是信用分析操作的串行化版本:voidUpdatePredictionsSequential(AccountRepository&accounts){for_each(accounts.begin(),accounts.end(),[](AccountRepository::value_type&record){Account&account=record.second;Trendtrend=Fit(account.Balances());doubleprediction=PredictIntercept(trend,(account.Balances().size()+g_predictionWindow));account.SeqPrediction()=prediction;account.SeqWarning()=prediction<account.GetOverdraft();});}在這個程序中,UpdatePredictionsSequential方法被用來處理賬戶庫中的每一個賬戶。而其中的Fit方法是一個工具函數(shù),它通過統(tǒng)計學中的最小二乘法根據(jù)一組(用戶)數(shù)據(jù)繪制出趨勢線。此外,F(xiàn)it方法是一個純函數(shù)過程即它不會改變任何東西的狀態(tài)。\h[1]該值是根據(jù)趨勢得出的三個月內的預測。如果預測值越過了負債限制(賬戶系統(tǒng)中的負債額是個負值),該賬戶就會被標志為審查狀態(tài)。\h[1]作者是在強調這個函數(shù)不會引入任何依賴關系?!g者注2.2.2parallel_for_each版的CreditReview并行版的信用度分析程序與串行版本幾乎相差無幾:voidUpdatePredictionsParallel(AccountRepository&accounts){parallel_for_each(accounts.begin(),accounts.end(),[](AccountRepository::value_type&record){Account&account=record.second;Trendtrend=Fit(account.Balances());doubleprediction=PredictIntercept(trend,(account.Balances().size()+g_predictionWindow));account.ParPrediction()=prediction;account.ParWarning()=prediction<account.GetOverdraft();});}如你所見,除了用parallel_for_each替代for_each外,UpdatePredictionsParallel方法的代碼與UpdatePredictionsSequential幾乎完全一致。2.2.3性能對比根據(jù)在一臺四核計算機上運行CreditReview的情況來看,parall_for_each版本相比于其串行版本快了近四倍。至于具體的時間變化情況,建議讀者在自己的計算機上運行一下網(wǎng)上所提供代碼來獲得第一手的數(shù)據(jù)。2.3模式變體上面的信用分析實例為我們展示了一個并行循環(huán)的典型用法,但它還可以有很多變體用法。本節(jié)將介紹其中最重要的幾個。盡管這些變體被使用的機會并不會太多,但是你應該知道它們是可行的。2.3.1提前退出循環(huán)在串行迭代中,退出循環(huán)的操作并不會令人感到陌生,而這類操作在并行循環(huán)中并不常見,但有時我們依然會用得著它。下面是一個串行情況下的做法:intn=……for(inti=0;i<n;i++){//……if(/·stoppingconditionistrue*/)break;}相比較而言,并行循環(huán)的情況會更復雜一些。因為其中往往會有不止一個步驟在同時運行,而且各步驟的執(zhí)行順序完全無法預知。但是,我們倒是可以通過取消任務組的方式來實現(xiàn)從一個并行循環(huán)中提前退出的效果。關于任務組,我們將會在第3章中詳細介紹。使用task_group::cancel方法可以幫助你提前退出一個并行循環(huán)。下面是一段如何提前跳出并行循環(huán)的示范代碼:vector<double>results=……intworkLoad=……task_grouptg;size_tfillTo=results.size()-5;fill(results.begin(),results.end(),-1.0);task_group_statusstatus=tg.run_and_wait([&]{parallel_for(0u,results.size(),[&](size_ti){if(i>fillTo)tg.cancel();elseresults[i]=DoWork(i,workLoad);});});如你所見,如果想要提前退出一個并行循環(huán),我們需要先為該循環(huán)建立一個任務組對象,然后讓該循環(huán)在任務組內執(zhí)行。這時候,如果你想退出循環(huán),只需要調用該任務組的cancel方法即可。不要忘了并行循環(huán)是無序執(zhí)行的,所以取消一個并行循環(huán)時并不能確保位置更靠后的迭代操作沒有先被運行。但是和串行循環(huán)不一樣的是,我們應該始終牢記并行循環(huán)中的步驟是無序執(zhí)行的。因此當你取消一個并行循環(huán)任務組的時候,并不能保證沒有更靠后的迭代在cancel被執(zhí)行之前已經(jīng)運行完成。2.3.2異常處理當一個并行循環(huán)中拋出未處理異常時,就不會再執(zhí)行新的步驟了。但在默認情況下,異常拋出時正在運行的其他迭代操作仍將繼續(xù)。一直到它們全部完成之后,該并行循環(huán)才會將異常上拋到調用它的線程中去。拋出一個未處理異??梢宰柚剐碌牡僮鏖_始。由于循環(huán)運行在并行環(huán)境中,所以可能會發(fā)生不止一個異常。如果遇上這種情況,并行循環(huán)會隨機拋出一個異常,而剩下的異常將不會被外界所察覺\h[1]。下面的代碼示范了如何在并行循環(huán)中執(zhí)行異常處理操作。vector<double>results=……try{size_tn=results.size();parallel_for(0u,n,[&results](size_ti){results[i]=DoWork(i,10);//throwsexception});}catch(ParallelForExampleExceptione){printf("Exceptioncaughtasexpected.\n");}\h[1]即無論并行循環(huán)中有多少異常發(fā)生,在循環(huán)所在線程中能看到的只有一個,而且是隨機的?!g者注2.3.3小型循環(huán)體的特殊處理如果循環(huán)體中只執(zhí)行了少量的工作,我們或許可以將這些迭代分配到更大的工作單元中去,以謀求更好的性能。這樣做是有原因的。當執(zhí)行一個循環(huán)時通常會引入兩種類型的成本:管理工作線程的成本和調用函數(shù)對象的成本。大多數(shù)情況下這些成本是微不足道的,但是,當循環(huán)體很小時這些開銷就不能忽略不計了。如果你有很多迭代,而每次迭代的工作量又很小的話,請考慮使用分組策略。parallel_for的其中一個重載版本允許我們指定索引值的步長(stepsize),當我們使用步長大于1的迭代時,就可以在并行循環(huán)中內嵌一個串行循環(huán)。這樣一來,外(并行)循環(huán)的每一次迭代就由對逐個索引值進行處理轉變?yōu)閷σ粋€索引值范圍進行處理。這種將迭代分組的方法可以讓我們避免一些并行循環(huán)固有的開銷。具體的實例代碼如下:size_tsize=results.size();size_trangeSize=size/(GetProcessorCount()*10);rangeSize=max(1,rangeSize);parallel_for(0u,size,rangeSize,[&results,size,rangeSize,workLoad](size_ti){for(size_tj=0;(j<rangeSize)&&(i+j<size);++j)results[i+j]=DoWork(i+j,workLoad);});相對于普通無分組的parallel_for函數(shù)來說,這種將數(shù)據(jù)分組的方式會導致更為復雜應用程序的邏輯。一旦遇上大工作量的迭代(或者當?shù)鷧^(qū)間的大小不均時),這個方法或許就無法幫助我們獲得較好的性能了。所以通常只有在經(jīng)過了深思熟慮之后,或者是在這種循環(huán)體極小而迭代次數(shù)很多的特定情況下,我們才會考慮采用這種更為復雜的語法。至于分組的數(shù)量,則一般取決于計算機上可用的內核數(shù)。默認條件下,分組數(shù)量大約維持在內核數(shù)的3到10倍是比較理想的。除此之外,另一種處理這種小型循環(huán)體的方法是,調用parallel_for_fixed和parallel_for_each_fixed。這兩個函數(shù)可以在并發(fā)運行時相關的示例程序包中找到。默認情況下,parallel_for和parallel_for_each都具有動態(tài)負載平衡(dynamicloadbalancing)的功能。一旦循環(huán)體中的工作量過小,這種負載平衡所帶來的開銷會對程序產生很大的影響。該示例包中的parallel_for_fixed和parallel_for_each_fixed則不會執(zhí)行這樣的負載平衡,因此它們在小循環(huán)體中或許能比parallel_for和parallel_for_each運行得更快一些。這兩組函數(shù)的另一個不同之處是,parallel_for和parallel_for_each會檢查每一次迭代的注銷情況,相比之下,parallel_for_fixed和parallel_for_each_fixed并不會在它們的子區(qū)間內執(zhí)行這種檢查。2.3.4并行度控制一般情況下,并行循環(huán)的迭代操作在內核上的具體分配是由系統(tǒng)自行管理的。但在某些特殊的時候,我們也需要取得更多的控制權。在并行循環(huán)模式中,我們經(jīng)常會在各種情況下用到這些模式變體。例如,為了模擬一些性能較差的硬件,我們需要通過降低程序的并行度來執(zhí)行一些性能測試;而如果循環(huán)中各次迭代都要花費大量時間來等待I/O輸入,就適于提高程序的并行度來提高執(zhí)行線程的數(shù)量,使之大于內核數(shù)。你可以控制一個并行循環(huán)中并發(fā)的活動線量的最大數(shù)量。所謂并行度,實質上是指我們在迭代時占用的內核數(shù)量。一般來說,并行度都是交由系統(tǒng)的底層組件自行管理的,例如parallel_for和parallel_for_each的函數(shù)實現(xiàn)、并發(fā)運行時的任務調度,以及操作系統(tǒng)中的線程調度等。這些組件都為各種條件下優(yōu)化系統(tǒng)的吞吐量做出了一定的貢獻。因此,盡管無法直接控制并行度,但我們可以通過控制執(zhí)行一個并行循環(huán)的線程數(shù)量來對其施加影響。而這需要我們去設置一下SchedulerPolicy類中MinConcurrency和MaxConcurrency策略。關于設置這些策略(policies)的更多信息,讀者可以參考附錄A中的相關內容。2.4反面模式所謂反面模式(Anti-Patterns),通常指的是一些具有警示性的反面故事,它為我們凸顯出了一些需要進行深思熟慮的事件和問題域。接下來,我們將會為你列舉一些在使用并行循環(huán)時需要注意的問題。2.4.1隱性循環(huán)體依賴錯誤分析了循環(huán)中的依賴關系往往是導致軟件缺陷的主要根源之一。因此,我們要留意所有并行循環(huán)體中不可見的隱性依賴,這些是非常容易出錯的地方。舉例來說,如果在非線程安全的情況下,我們在各個并行迭代操作之間共享了一個類實例的話,就很可能會遇上這種微妙的依賴性。同樣,你也要留意在lambda表達式的封閉域中使用引用變量來共享狀態(tài)的問題。即使各循環(huán)體之間彼此并不完全獨立,我們依然是可以使用并行循環(huán)的。只不過,在這種情況下,我們必須確保所有的共享變量是受保護的,并且是可以同步的。同時,我們還必須對自己所引入的任何同步化機制在性能影響方面有充分的認知。盡管這種機制的引入可能會極大地降低并發(fā)程序的性能,但如果你因此而忽略了它的必要性,同樣會導致程序中出現(xiàn)災難性的并且難以重現(xiàn)的bug。如果循環(huán)體之間并不存在任何依賴關系——例如執(zhí)行求和運算——你或許就可以考慮我們將在第4章中介紹的并行循環(huán)變體。2.4.2少量迭代的小循環(huán)體如果我們只是在很小的并行循環(huán)體中處理一些很有限的數(shù)據(jù)元素,性能或許并不會得到多少改善,這種情況下并行循環(huán)本身的成本反而會成為主要開銷。顯然,如果僅僅是簡單地把每個串行的for循環(huán)改為parallel_for循環(huán),并不一定就能獲得好的結果。2.4.3重復輸入性枚舉在使用parallel_for_each的過程中,如果輸入中有多個指針或引用變量指向了同一個對象實體,往往就意味著我們的代碼中可能存在不安全競爭的情況。因為如果同一個對象引用(objectreference,即指向同一個類實例的指針或引用)在循環(huán)輸入中不止一次地出現(xiàn),就很可能會產生兩個并行線程同時更新一個對象的問題。請不要在并行循環(huán)中反復使用同一個實體的副本,如果有一個對象不止一次地出現(xiàn)在某個循環(huán)的輸入中,該對象就很有可能會被兩個并行線程同時更新。2.4.4基于協(xié)同性阻塞的交叉調度在一個并行循環(huán)中,如果它的每次迭代都要執(zhí)行協(xié)同性阻塞(cooperativeblocking)\h[1],就很可能會使任務調度器中所產生的線程數(shù)量多得超乎想象。因此,我們應該盡可能少地在此類循環(huán)中使用協(xié)同性阻塞操作。\h[1]關于協(xié)同性阻塞的內容請參考第3章中的內容。——譯者注2.5相關模式并行循環(huán)其實還是并行聚合模式的基礎模式。而后者正是我們將要在第4章中介紹的內容。2.6練習題1)在下面的這些問題中,有哪一個能適用于本章所介紹的并行循環(huán)技術?a)對內存中的一個擁有100萬個數(shù)字元素的數(shù)組進行排序。b)讀取一個文本文件,并按字母順序輸出每一行的單詞。c)對一個集合中的所有數(shù)字求和,并輸出最終的運算結果。d)將兩個集合中的數(shù)字兩兩相加,并將每對數(shù)字的相加值輸出到第三個集合中。e)對一個文本文件集合中出現(xiàn)的每個單詞執(zhí)行統(tǒng)計計數(shù)。f)找出一個文本文件集合中出現(xiàn)頻率最高的單詞。2)從上題中選擇一個你認為合適的問題,分別用串行循環(huán)和并行循環(huán)實現(xiàn)一個解決方案。3)對CodePlex站點中的CreditReview實例執(zhí)行性能分析。用命令行選項獨立地變換迭代次數(shù)(即賬戶數(shù))和每個迭代的工作量(即信用記錄中的月份數(shù))。并記錄下該程序三個版本所有的執(zhí)行時間報告。然后,變換賬戶數(shù)和月份數(shù)的各種不同組合在不同內核數(shù),以及不同執(zhí)行環(huán)境(從其他應用程序中載入)的各種計算機上進行反復測試。2.7補充閱讀在這本書中,幾乎所有的實例都運用了MicrosoftVisualC++中的功能特性和程序庫,因此關于這些功能、程序庫以及l(fā)ambda表達式的詳細資料,MSDN自然是首要的參考來源。此外,Mattson等人也在自己的書中介紹了一些關于并行編程的軟件設計模式,值得一提的是,這些模式并不局限于特定的語言和程序庫。最后,Messmer的文章也值得你看看,其中介紹了幾個與本章相關的模式,以及一些關于如何在PPL中使用并行循環(huán)的建議?!attson,T.G.,B.A.Sanders,andB.L.Massingill.PatternsforParallelProgramming.Addison-Wesley,2004.·Mattson,T.G.,"UseandAbuseofRandomNumbers"(video),F(xiàn)eb14,2008,/en-us/videos/timmattson-use-and-abuse-of-random-numbers/.·Messmer,B.,ParallelPatternsLibrary,AsynchronousAgentsLibrary,&ConcurrencyRuntime:PatternsandPractices,2010./downloads/en/confirmation.aspx?displaylang=en&FamilyID=0e70b21e-3f10-4635-9af2-e2f7bddba4ae.·MSDN,LambdaExpressionsinC++,http://msdn./en-us/library/dd293608.aspx.第3章并行任務并行任務是一種可以讓我們在同一時間內執(zhí)行的異步操作(asynchronousoperations)。這種方法也被稱為任務并行化。第2章所介紹的并行循環(huán)模式,實質上是一種實現(xiàn)用單一操作處理多組數(shù)據(jù)的并行技術,有時也稱為數(shù)據(jù)并行化(dataparallelism)。第3章將要為你介紹的是,面對多個同時運行的異步操作時所應該選擇的處理技術。在這種情況下,如果我們能實現(xiàn)任務的并行化執(zhí)行,就可以在程序控制流中實現(xiàn)類似于fork\h[1]這樣的操作。這種做法通常也稱為任務并行化(taskparallelism)。正因為如此,并行任務模式有時候也會稱為Fork/Join模式或者Master/Worker模式。數(shù)據(jù)并行化和任務并行化就好像位于我們技術光譜的兩端\h[2]。前者通常側重于處理單一操作的多重輸入;而后者則往往側重于處理多個操作,并且每個操作中都有屬于自己的輸入。在并行模式庫(PPL)中,我們一般會通過task_group類的方法來啟動并管理任務。你可以在ppl.h頭文件中找到task_group類的相關聲明。在該類中,run方法主要用于創(chuàng)建新的任務,并對其進行調度;而wait方法則主要用于使程序暫停下來,以等待某個任務組對象完成它所有的任務。從fork/join組操作角度來看,這里的run方法就相當于fork操作,而wait方法則基本等同于join。在PPL中,并行任務是通過task_group類來管理的。任務的調度是并行任務模式中的一個重點內容。這與線程不同,新任務啟動之后通常并不會立即執(zhí)行。而是先被移到一個工作隊列中。等處理器有空閑資源時,再由相關的調度器將它們從工作隊列中調出來運行。因此,只要我們確保任務足夠多,并且這些任務之間不存在序列化依賴關系(serializingdependency),程序性能往往就能隨著可用內核數(shù)的增多而改善。在這里,我們又有了一次體會潛在并行化概念的機會。還記得嗎?這個概念在第一章中曾被我們反復提及。此外,對基于任務模式的應用程序來說,另一個重點內容就是異常處理機制。在PPL中,一個在任務執(zhí)行期間發(fā)生的未處理異常通常會被推遲發(fā)現(xiàn)。例如,某個異常通常會被推遲到task_group::wait方法被調用時自動發(fā)現(xiàn)。到那時,該異常會在當前所調用的wait方法中再次被拋出,這種方式讓我們得以在并行程序中使用串行式的異常處理方法。在PPL中,任務會推遲異常的上報,直至調用任務組方法wait時才會再次拋出。3.1基本用法盡管每一個任務實際上都是一組串行化操作,但任務本身的執(zhí)行是實現(xiàn)并行化的。請看下面這兩行串行的代碼:DoLeft();DoRight();讓我們先來假設一下,如果這里的DoLeft和DoRight都是可以獨立執(zhí)行的函數(shù),即它們不會一個方法讀另一個方法寫同樣的本地內存區(qū)域或者磁盤文件。我們就可以將它們并行化。為了實現(xiàn)這一點,我們需要調用Concurrency命名空間中的parallel_invoke函數(shù)。像下面這樣:parallel_invoke([](){DoLeft();},[](){DoRight();});Concurrency命名空間中的parallel_invoke函數(shù)能在系統(tǒng)中建立起一組并行任務,并會等到這些任務完成之后再繼續(xù)。parallel_invoke函數(shù)是并行任務模式中最簡單的實現(xiàn)形式了。該函數(shù)會將參數(shù)列表中的每一個lambda表達式都視作一個新的并行任務,并為它們創(chuàng)建一個新的任務組來執(zhí)行。并在所有任務完成之后返回。正因為如此,parallel_invoke函數(shù)參數(shù)列表中的那些函數(shù)也稱為工作函數(shù)(workfunction)。順帶一提,有些parallel_invoke函數(shù)的重載版本所支持的工作函數(shù)可以達到9個之多。需要注意的是,在我們使用并行任務時,不能把設計思路建立在所有任務都會被立即執(zhí)行的基礎上。因為,并行任務的運行時機幾乎完全取決于它當時所在的工作負載和系統(tǒng)配置,也就是說,任務調度器既有可能會依次調用它們,也有可能讓它們同時運行。關于任務調度的細節(jié)問題,后面再詳細介紹。在parallel_invoke中,任務函數(shù)除了以正常形式完成調用外,也有可能會因拋出異常而提前結束。如果在parallel_invoke函數(shù)運行期間,某個工作函數(shù)拋出了異常,通常會推遲到所有任務執(zhí)行完成之后再上拋。如果拋出異常的工作函數(shù)不止一個,運行時環(huán)境會隨機選擇其中的一個來上拋,而其余的異常將不會被外部發(fā)現(xiàn)\h[3]。關于異常處理的詳細信息及其相關的代碼示例,我們將會在3.3.3節(jié)中進一步說明。從本質上說,parallel_invoke函數(shù)的主要作用就是新建一系列任務并等待這些任務完成執(zhí)行。因而,我們完全可以自己建立一個任務組對象,并通過調用它的run和wait方法來實現(xiàn)同樣的功能。例如:task_grouptg;tg.run([](){DoLeft();});tg.run([](){DoRight();});tg.wait();任務組對象的run方法主要用于創(chuàng)建一個任務并完成它的調度與執(zhí)行;而wait方法則主要用于阻塞當前環(huán)境,以待任務組執(zhí)行完所有的任務。上例通過task_group類的run方法新建了一個任務,并完成了相關的執(zhí)行調度。實際上,run方法的參數(shù)是一個供任務執(zhí)行時所調用的函數(shù),它可以是一個lambda表達式、一個函數(shù)指針或者一個函數(shù)對象。換句話說,該參數(shù)可以是任何支持形如voidoperator()()函數(shù)調用運算符的對象。task_group::run方法每新建一個任務,就會將該任務加入到一個工作隊列中,以備最終執(zhí)行。但是只有在任務調度器將其調出隊列時才會執(zhí)行它,而這種出隊操作可以現(xiàn)在發(fā)生,也可以在將來的某個時間點上發(fā)生。如果傳遞給task_group::run方法的參數(shù)引用了某個支持operator()接口的類實例。那么,我們就必須有效地管理該函數(shù)對象所用的內存,確保該對象在任務組的wait方法返回之后才被銷毀。而lambda表達式或者指向靜態(tài)函數(shù)的函數(shù)指針,并不需要顯式銷毀。從這點來說,這確實比functors類類型(class-type)要方便得多。我們能通過調用任務組對象的wait方法來令其等待該任務組中的任務完成之后再返回。或許有時候我們也可以將run和wait的操作步驟合二為一,
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
- 4. 未經(jīng)權益所有人同意不得將文件中的內容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
- 6. 下載文件中如有侵權或不適當內容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 2025婦女權益保障集體合同
- 《2025項目合作合同書》
- 2024-2025學年人教版PEP四年級英語下冊期末試卷(3)(含答案含聽力原文無音頻)
- 2025標準寫字樓租賃合同模板下載
- 2025典范保險合同模板
- 2025裝飾裝修設計合同爭議
- 2025年供氣合同模板范文
- 2025私人房屋買賣合同書范本
- 2025在線簽訂勞動合同的操作流程
- 2025年網(wǎng)絡廣告投放合同范本
- 即時通訊系統(tǒng)建設方案
- 動車乘務實務知到智慧樹章節(jié)測試課后答案2024年秋陜西交通職業(yè)技術學院
- 胎盤植入課件講義版
- 山東鐵投集團招聘筆試沖刺題2025
- 2025年江蘇鹽城東方集團招聘筆試參考題庫含答案解析
- 2021版中醫(yī)疾病醫(yī)保對應中醫(yī)疾病醫(yī)保2
- 政府績效評估 課件 蔡立輝 第1-5章 導論 -政府績效評估程序
- 食堂負責人崗位職責
- 車間排產計劃培訓
- 無菌醫(yī)療器械培訓課件
- 2024-2030年中國煤礦電機行業(yè)供需狀況發(fā)展戰(zhàn)略規(guī)劃分析報告
評論
0/150
提交評論