版權(quán)說(shuō)明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)
文檔簡(jiǎn)介
VisualC++并行編程實(shí)戰(zhàn)多喝架構(gòu)下分工與協(xié)作的設(shè)計(jì)模式目錄\h第1章引言\h1.1潛在并行化的重要意義\h1.2分解、協(xié)調(diào)、可擴(kuò)展性共享\h1.2.1理解任務(wù)\h1.2.2協(xié)調(diào)任務(wù)\h1.2.3可擴(kuò)展性數(shù)據(jù)共享\h1.2.4設(shè)計(jì)方法\h1.3選擇正確的設(shè)計(jì)模式\h1.4關(guān)于術(shù)語(yǔ)\h1.5并行的局限\h1.6一些建議\h1.7練習(xí)題\h1.8更多資源\h第2章并行循環(huán)\h2.1基本用法\h2.1.1并行版的for循環(huán)\h2.1.2parallel_for_each\h2.1.3期望為何\h2.2實(shí)例示范\h2.2.1串行版的CreditReview\h2.2.2parallel_for_each版的CreditReview\h2.2.3性能對(duì)比\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重復(fù)輸入性枚舉\h2.4.4基于協(xié)同性阻塞的交叉調(diào)度\h2.5相關(guān)模式\h2.6練習(xí)題\h2.7補(bǔ)充閱讀\h第3章并行任務(wù)\h3.1基本用法\h3.2實(shí)例示范\h3.3模式變體\h3.3.1基于協(xié)同性阻塞的任務(wù)協(xié)調(diào)\h3.3.2取消一個(gè)任務(wù)組\h3.3.3異常處理\h3.3.4預(yù)測(cè)性執(zhí)行\(zhòng)h3.4反面模式\h3.4.1閉包中的變量捕獲\h3.4.2計(jì)劃外的取消狀態(tài)傳遞\h3.4.3同步化成本\h3.5設(shè)計(jì)注意事項(xiàng)\h3.5.1任務(wù)組調(diào)用約定\h3.5.2任務(wù)與線程\h3.5.3如何調(diào)度任務(wù)\h3.5.4結(jié)構(gòu)化任務(wù)組及任務(wù)處理\h3.5.5輕量級(jí)任務(wù)\h3.6練習(xí)題\h3.7補(bǔ)充閱讀\h第4章并行聚合\h4.1基本用法\h4.2實(shí)例示范\h4.3模式變體\h4.3.1基于小型循環(huán)體的考慮\h4.3.2Combinable對(duì)象的其他用處\h4.4設(shè)計(jì)注意事項(xiàng)\h4.5相關(guān)模式\h4.6練習(xí)題\h4.7補(bǔ)充閱讀\h第5章Future\h5.1基本用法\h5.2實(shí)例示范:Adatum金融儀表盤(pán)\h5.2.1業(yè)務(wù)對(duì)象\h5.2.2分析引擎\h5.3模式變體\h5.3.1取消Future對(duì)象\h5.3.2消除瓶頸\h5.3.3在運(yùn)行時(shí)修改任務(wù)圖\h5.4設(shè)計(jì)注意事項(xiàng)\h5.4.1分解到future對(duì)象中去\h5.4.2函數(shù)式風(fēng)格\h5.5相關(guān)模式\h5.5.1管道模式\h5.5.2Master/Worker模式\h5.5.3動(dòng)態(tài)任務(wù)并行化模式\h5.5.4離散事件模式\h5.6練習(xí)題\h第6章動(dòng)態(tài)任務(wù)并行化\h6.1基本用法\h6.2實(shí)例示范\h6.3模式變體\h6.3.1非空while循環(huán)體的并行化\h6.3.2在掛起等待環(huán)境中添加任務(wù)\h6.4練習(xí)題\h6.5補(bǔ)充閱讀\h第7章管道\h7.1消息塊類型概述\h7.2基本用法\h7.3實(shí)例示范\h7.3.1串行化的圖形處理\h7.3.2圖形管道\h7.3.3性能特征\h7.4模式變體\h7.4.1異步管道\h7.4.2管道中的取消操作\h7.4.3管道中的異常處理\h7.4.4多生產(chǎn)者作用下的負(fù)載平衡\h7.4.5管道與流的關(guān)系\h7.5反面模式\h7.5.1在管道各階段之間進(jìn)行大量的數(shù)據(jù)拷貝\h7.5.2管道階段中的工作量過(guò)小\h7.5.3在消息傳遞時(shí)忘記使用隔離技術(shù)\h7.5.4無(wú)限期的等待\h7.5.5無(wú)限制的隊(duì)列增長(zhǎng)\h7.5.6更多信息\h7.6設(shè)計(jì)注意事項(xiàng)\h7.7關(guān)聯(lián)模式\h7.8練習(xí)題\h7.9補(bǔ)充閱讀\h附錄A任務(wù)調(diào)度器與資源管理器\hA.1資源管理器\hA.1.1為什么需要資源管理器\hA.1.2資源管理器的工作方式\hA.1.3動(dòng)態(tài)資源管理\hA.1.4超額預(yù)訂內(nèi)核\hA.1.5查詢環(huán)境\hA.2任務(wù)的種類\hA.2.1輕量級(jí)任務(wù)\hA.2.2基于PPL創(chuàng)建的任務(wù)\hA.3任務(wù)調(diào)度器\hA.3.1管理任務(wù)調(diào)度器\hA.3.2調(diào)度算法\hA.3.3使用Context類實(shí)現(xiàn)和調(diào)度器的通信\hA.3.4設(shè)置調(diào)度策略\hA.4反面模式\hA.4.1多重資源管理器\hA.4.2資源管理的開(kāi)銷(xiāo)\hA.4.3來(lái)自內(nèi)聯(lián)任務(wù)的非計(jì)劃性超額預(yù)訂\hA.4.4因線程饑餓而導(dǎo)致的死鎖\hA.4.5忽略進(jìn)程關(guān)聯(lián)碼\hA.5引用資料\h附錄B并行應(yīng)用程序的調(diào)試與分析\hB.1并行任務(wù)窗口與并行堆棧窗口\hB.2斷點(diǎn)與內(nèi)存分配\hB.3并發(fā)可視化工具\(yùn)hB.4可視化模式\hB.4.1超額預(yù)訂\hB.4.2鎖的競(jìng)爭(zhēng)與串行化\hB.4.3負(fù)載平衡\hB.5補(bǔ)充閱讀\h附錄C技術(shù)總覽\h補(bǔ)充閱讀\h術(shù)語(yǔ)表第1章引言在多核體系環(huán)境中,CPU儀表盤(pán)上經(jīng)常顯示著一個(gè)問(wèn)題,即計(jì)算機(jī)上只有一個(gè)內(nèi)核在滿負(fù)荷運(yùn)行,而其他內(nèi)核則都被閑置了,這使得我們的程序看上去似乎受到了CPU性能的限制,但事實(shí)上,這才僅僅利用了多核系統(tǒng)中的一小部分性能而已。那么,這個(gè)問(wèn)題有更好的解決方案嗎?答案是簡(jiǎn)單而明確的,那就是并行編程(parallelprogramming)?,F(xiàn)如今,我們?cè)絹?lái)越多地發(fā)現(xiàn),自己曾一直習(xí)慣于寫(xiě)的并且也為所有程序員熟悉的那種串行化代碼(sequentialcode)顯然已經(jīng)無(wú)法滿足用戶的性能需求了。為了進(jìn)一步提升系統(tǒng)中CPU資源的使用率,我們需要將應(yīng)用程序分解成能各自為戰(zhàn)的執(zhí)行片斷(piece),以便使它們能夠在同一時(shí)間內(nèi)運(yùn)行。并行編程使我們可以同一時(shí)間內(nèi)調(diào)用多個(gè)內(nèi)核資源,這能有效地提升應(yīng)用程序的運(yùn)行速度。當(dāng)然,說(shuō)起來(lái)容易做起來(lái)難。畢竟并行編程一直被認(rèn)為是專家們的領(lǐng)域,同時(shí)也常被視為是一個(gè)雷區(qū),其中隱藏著各種難以重現(xiàn)的、詭異的軟件缺陷。幾乎每個(gè)程序員都會(huì)跟你老生常談一個(gè)關(guān)于神秘bug如何導(dǎo)致并行程序運(yùn)行結(jié)果與期望大相徑庭的故事。也許,那些故事讓你對(duì)自己所面臨的困難有了一些清醒的認(rèn)識(shí)。但幸運(yùn)的是,你不是一個(gè)人在戰(zhàn)斗,并行模式庫(kù)(ParallelPatternsLibrary,PPL)和異步代理庫(kù)(AsynchronousAgentsLibrary)引入了一個(gè)全新的并行編程模型,該模型大大地簡(jiǎn)化了編寫(xiě)并行程序的工作。當(dāng)然,這些簡(jiǎn)化的背后隱藏著一系列精致而復(fù)雜的算法,它們能夠很好地適應(yīng)于多核體系(multicorearchitecture)中的動(dòng)態(tài)分布式計(jì)算。此外,VisualStudio2010開(kāi)發(fā)系統(tǒng)中還內(nèi)置了一系列的調(diào)試器與分析工具,以便支持這個(gè)全新的并行編程模型。如果覺(jué)得這里的內(nèi)容太過(guò)拖沓,你也可以根據(jù)自己的需要直接參考1.3節(jié)。另外,你還可以求助于久經(jīng)考驗(yàn)的設(shè)計(jì)模式(designpattern),在本書(shū)中,我們介紹了一些最重要的、也是最常用的并行編程模式,并提供相應(yīng)的基于PPL編寫(xiě)的可執(zhí)行代碼示例,對(duì)其進(jìn)行輔助說(shuō)明。至于這本書(shū)的使用問(wèn)題,我們建議你最好先大致瀏覽一遍接下來(lái)的章節(jié)中提到的6個(gè)模式,看看你的問(wèn)題中是否有一些特征與這些模式是相匹配的。如果答案是肯定的,就說(shuō)明這些模式(包括相應(yīng)的代碼示例)是值得你去深入地學(xué)習(xí)、研究的。盡管編寫(xiě)并行程序確實(shí)是出了名的難,但你不是一個(gè)人在戰(zhàn)斗。由于大部分并行程序都應(yīng)該或多或少地遵守這些設(shè)計(jì)模式,因此,我們通常很容易就能找到一個(gè)相匹配的模式來(lái)解決特定的問(wèn)題。如果真的沒(méi)有一個(gè)用武之地,那很可能是因?yàn)槲覀冇錾狭艘粋€(gè)難度很大的問(wèn)題,這時(shí)候我們或許就需要去尋求專家或者專業(yè)文獻(xiàn)的幫助了。在本書(shū)中,所有的樣例代碼都可以在下面的網(wǎng)站下載:/。1.1潛在并行化的重要意義本書(shū)中提到的所有模式都旨在幫助我們發(fā)掘出程序中潛在的并行化。所謂的潛在并行性(potentialparallelism),實(shí)質(zhì)上是指,如果應(yīng)用程序得到了并行硬件的支持,它就能運(yùn)行得更快;即使硬件不支持并行化,它的性能也理應(yīng)與其等價(jià)的串行程序相差無(wú)幾。只要代碼的架構(gòu)設(shè)計(jì)正確,運(yùn)行時(shí)環(huán)境就應(yīng)該能根據(jù)計(jì)算機(jī)上具體的工作負(fù)載做出自動(dòng)調(diào)整。當(dāng)然,本書(shū)中的這些模式也只能對(duì)潛在并行化提供一般性的提示,畢竟誰(shuí)也無(wú)法保證程序在任何情況下都能實(shí)現(xiàn)并行執(zhí)行。但是,挖掘這種潛在并行化正是PPL編程模型的核心思想所在,值得我們?yōu)榇诉M(jìn)一步說(shuō)明。這里說(shuō)明了程序中的潛在并行化無(wú)論在多核還是單核的情況下,都可以讓處理器中的所有內(nèi)核保持有效負(fù)荷。在常見(jiàn)的并行應(yīng)用程序中,有不少是為特定的硬件而編寫(xiě)的。以游戲控制平臺(tái)為例,游戲程序員可能事先就對(duì)程序在運(yùn)行時(shí)可用的硬件資源了如指掌,包括處理器內(nèi)核數(shù)量和內(nèi)存架構(gòu)等細(xì)節(jié)。當(dāng)然,對(duì)于硬件平臺(tái)的全面掌握也是嵌入式應(yīng)用程序開(kāi)發(fā)的基本特征,例如工程進(jìn)度控制(industrialprocesscontrol)程序就是這樣。不過(guò),這也決定了這些程序的生命周期必須要和它們所依賴的硬件保持一致。\h[1]相反,如果是在桌面工作站或者服務(wù)器這樣的通用計(jì)算平臺(tái)上編寫(xiě)程序,我們對(duì)硬件特性的估測(cè)能力就要打些折扣了。畢竟,我們不可能總能知道有多少內(nèi)核可供使用,也不太可能預(yù)先知道會(huì)有什么其他軟件將與程序共同運(yùn)行。不要對(duì)應(yīng)用程序的并行度進(jìn)行硬編碼(hardcode),因?yàn)檫\(yùn)行時(shí)可用的內(nèi)核數(shù)量往往是無(wú)法預(yù)知的。即便應(yīng)用程序最初的運(yùn)行環(huán)境是確定的,隨著時(shí)間推移,它也是會(huì)發(fā)生變化的。在過(guò)去,程序員總是假定自己的程序在下一代硬件中會(huì)運(yùn)行得更快。你現(xiàn)在依然可以這樣認(rèn)為,因?yàn)樘幚砥髦黝l終究還是會(huì)越來(lái)越快的。不過(guò),對(duì)于現(xiàn)在的多核處理器而言,主頻增長(zhǎng)的步伐已經(jīng)開(kāi)始慢了下來(lái),取而代之的是處理器中的內(nèi)核越來(lái)越多。如果想讓我們的應(yīng)用程序在多核世界中分享到硬件進(jìn)步所帶來(lái)的利益,就必須要對(duì)程序設(shè)計(jì)模型做出相應(yīng)地調(diào)整,以便程序在今后能運(yùn)行在多核計(jì)算機(jī)上。同時(shí),我們還應(yīng)該致力于發(fā)掘程序中潛在的并行計(jì)算能力,以便體現(xiàn)出某種“前瞻性”。處理器的發(fā)展趨勢(shì)不再是主頻速度越來(lái)越快,取而代之的是增加越來(lái)越多的內(nèi)核。最后,我們還有必要為可能的意外情況做一些準(zhǔn)備,畢竟,總會(huì)有些用戶無(wú)法獲得最新的硬件,這時(shí)候,我們需要確保并行程序在單核計(jì)算機(jī)上的性能至少和只用串行代碼編寫(xiě)的程序相差無(wú)幾,換句話說(shuō),就是要讓?xiě)?yīng)用程序在單核和多核之間保持性能的可擴(kuò)展性,讓其擁有更強(qiáng)的硬件適應(yīng)能力,并保持前瞻性。而這正是發(fā)掘潛在并行化的目的所在。一個(gè)設(shè)計(jì)良好的并行程序在單核情況下的效率應(yīng)該與相應(yīng)的串行程序相差無(wú)幾。例如,我們將在第2章中介紹并行循環(huán)模式就是一個(gè)發(fā)掘潛在并行化的典型示例。對(duì)于一個(gè)將要迭代100萬(wàn)次的for循環(huán)來(lái)說(shuō),如果能確定其中的每個(gè)迭代體都是彼此獨(dú)立,我們就完全有理由將這些迭代分解為并行計(jì)算,把工作分配到各個(gè)閑置的處理器內(nèi)核中。顯然,具體的分配工作取決于可用內(nèi)核的數(shù)量,多數(shù)情況下,該循環(huán)的速度將與內(nèi)核數(shù)成正比。\h[1]軟件的生命周期高度依賴于硬件通常被視為軟件設(shè)計(jì)的一大失敗,這會(huì)導(dǎo)致生產(chǎn)力的浪費(fèi)?!g者注1.2分解、協(xié)調(diào)、可擴(kuò)展性共享在這本書(shū)中,所有的模式都有一些共同的主題。你將會(huì)從后面章節(jié)中看到一個(gè)并行程序從設(shè)計(jì)到實(shí)現(xiàn)的全過(guò)程,主要內(nèi)容包括以下三個(gè)方面:將工作分解成各個(gè)獨(dú)立單元(即任務(wù))的方法;協(xié)調(diào)并行任務(wù)運(yùn)行的途徑;以及共享任務(wù)所需數(shù)據(jù)的可擴(kuò)展性技術(shù)。此外,這本書(shū)所討論的模式都屬于設(shè)計(jì)模式,因此,當(dāng)我們?cè)O(shè)計(jì)并實(shí)現(xiàn)算法,或者思考程序的整體結(jié)構(gòu)時(shí),都可以應(yīng)用它們。盡管這些示例應(yīng)用程序的規(guī)模都很小,但其中的原則同樣適用于大型應(yīng)用程序的架構(gòu)設(shè)計(jì)。1.2.1理解任務(wù)所謂任務(wù)(task),實(shí)質(zhì)上是指一系列相互協(xié)作、共同完成某個(gè)較大操作的串行化操作序列。當(dāng)我們?cè)O(shè)計(jì)一個(gè)并行程序的結(jié)構(gòu)時(shí),為任務(wù)找到一個(gè)合適的劃分粒度,以便它能有效地利用硬件資源是一個(gè)十分重要的步驟。如果分得太精細(xì),任務(wù)的管理成本將會(huì)成為程序的主要開(kāi)銷(xiāo);而如果分得太粗糙,就可能會(huì)導(dǎo)致處理器的一些內(nèi)核因無(wú)法分配到工作而被閑置,令程序失去了一些并行化的機(jī)會(huì)。一般情況下,任務(wù)應(yīng)該分得盡可能大,確保它們彼此獨(dú)立,并且讓所有的內(nèi)核都保持有效負(fù)荷。除了任務(wù)粒度之外,任務(wù)調(diào)度的方式也是一項(xiàng)不得不考慮的因素。要符合以上所有的目標(biāo),我們就必須在設(shè)計(jì)上做出一定權(quán)衡,而只有當(dāng)我們對(duì)應(yīng)用程序的算法和結(jié)構(gòu)有了充分的認(rèn)知,才能在分解問(wèn)題過(guò)程中將任務(wù)劃分得恰到好處。任務(wù)是串行化的工作單元,它們應(yīng)該被分得盡可能大,在彼此獨(dú)立的基礎(chǔ)上,數(shù)量也要足以讓所有內(nèi)核保持有效負(fù)荷。為了讓這些設(shè)計(jì)原則顯得更具體一些,我們來(lái)看一個(gè)射線追蹤程序?qū)嵗?。在這個(gè)應(yīng)用中,射線追蹤器(raytracer)負(fù)責(zé)跟蹤一個(gè)場(chǎng)景中的每條光線軌跡,并將這些軌跡合成一張軌跡圖。在這個(gè)問(wèn)題中,如果我們使用單個(gè)模擬射線本身作為這個(gè)并行計(jì)算的單位任務(wù),粒度上是恰到好處的。如果任務(wù)粒度再小一些,比如將單個(gè)模擬器再進(jìn)行分解,就只會(huì)增加程序的開(kāi)銷(xiāo)。因?yàn)槟M器的數(shù)量已經(jīng)足以讓所有內(nèi)核保持有效負(fù)荷。而如果各個(gè)任務(wù)的持續(xù)時(shí)間差距過(guò)大,我們就需要?jiǎng)澐殖龈嗟娜蝿?wù)來(lái)填補(bǔ)它們之間的空隙。記住,任務(wù)不等同于線程(thread),任務(wù)和線程在程序調(diào)度方面有著很大的不同。任務(wù)比線程擁有更多的潛在并行化概念。一個(gè)新的線程必然會(huì)增加應(yīng)用程序的并發(fā)性,而一個(gè)新的任務(wù)只是增加了這種可能性,這種可能性只有當(dāng)程序獲得足夠多的內(nèi)核資源時(shí)才能實(shí)現(xiàn)。此外,將目標(biāo)任務(wù)劃分成這些大而少的子任務(wù)還有另一個(gè)優(yōu)點(diǎn),即較大的任務(wù)通常彼此間具有更多的獨(dú)立性,擁有需要共享的局部變量和域的可能性也更小。然而,遺憾的是,在一些依賴于不穩(wěn)定的、大型的對(duì)象圖結(jié)構(gòu)(objectgraph)\h[1]的應(yīng)用程序中,情況或許正好相反,這些程序中往往會(huì)存在一些擁有許多公開(kāi)的類、方法以及屬性的大型對(duì)象模型。這種情況下,任務(wù)越大,遇到意外的數(shù)據(jù)共享或其他副作用的可能性就越高??偠灾?,你的最終目標(biāo)是問(wèn)題分解成各個(gè)獨(dú)立的任務(wù),確保它們之間沒(méi)有數(shù)據(jù)共享,并且任務(wù)的數(shù)量足以讓處理器的所有內(nèi)核保持有效負(fù)荷。此外,當(dāng)你考慮內(nèi)核數(shù)量的時(shí)候,也應(yīng)該把下一代硬件中可能擁有更多內(nèi)核的因素考慮進(jìn)去。\h[1]這是一種表示對(duì)象引用關(guān)系的數(shù)據(jù)結(jié)構(gòu),后面會(huì)進(jìn)一步提及。——譯者注1.2.2協(xié)調(diào)任務(wù)通常多個(gè)任務(wù)需要同時(shí)運(yùn)行。彼此不存在依賴的任務(wù)可以并行運(yùn)行。當(dāng)然,也會(huì)有一些任務(wù)必須等待別的任務(wù)完成之后才能開(kāi)始。任務(wù)的執(zhí)行順序和并行化程度取決于應(yīng)用程序的內(nèi)在算法。約束來(lái)自于如控制流(算法的步驟流程)和數(shù)據(jù)流(有效的輸入輸出)。協(xié)調(diào)任務(wù)的機(jī)制有很多,具體的運(yùn)用取決于我們所選擇的并行模式。例如第7章中的管道模式是采用消息機(jī)制來(lái)協(xié)調(diào)任務(wù)的。而無(wú)論你選擇哪一種機(jī)制來(lái)協(xié)調(diào)任務(wù),都必須充分了解任務(wù)之間的依賴關(guān)系。只有這樣,我們才有可能獲得一個(gè)成功的設(shè)計(jì)。1.2.3可擴(kuò)展性數(shù)據(jù)共享通常來(lái)說(shuō),任務(wù)之間總免不了要共享數(shù)據(jù)。但問(wèn)題是,這些任務(wù)在并行化運(yùn)行的過(guò)程中很有可能會(huì)對(duì)同一塊內(nèi)存區(qū)域執(zhí)行競(jìng)爭(zhēng)式訪問(wèn)和更新,使得相關(guān)的數(shù)據(jù)變得非常不可靠。這種不在我們計(jì)劃內(nèi)的數(shù)據(jù)競(jìng)爭(zhēng)所帶來(lái)的后果往往是災(zāi)難性的。要想避免這個(gè)問(wèn)題,解決方案之一就是使用線程同步技術(shù)。也許,你對(duì)那些通過(guò)阻塞線程來(lái)實(shí)現(xiàn)的同步并發(fā)線程技術(shù)并不陌生,比如鎖、原子性對(duì)比交換操作、信號(hào)量等都屬于這一類技術(shù)。這些技術(shù)也確實(shí)都能有效地實(shí)現(xiàn)訪問(wèn)共享資源。事實(shí)上,我們對(duì)于數(shù)據(jù)共享的第一反應(yīng)似乎也依然是加鎖這一類的同步化(synchronization)機(jī)制。但是,此類機(jī)制的引入必然會(huì)降低程序的并行性(parallelism),因?yàn)橥交旧砭褪谴谢╯erialization)的一種表現(xiàn)形式。而且,頻繁的鎖權(quán)爭(zhēng)奪操作很可能會(huì)導(dǎo)致任務(wù)無(wú)暇去做它們應(yīng)做的事情,所以如果在程序中使用鎖這樣的技術(shù)來(lái)實(shí)現(xiàn)同步化,是非常容易出錯(cuò)的。幸運(yùn)的是,有一些數(shù)據(jù)共享技術(shù)可以在不降低性能或增加錯(cuò)誤率的情況下使用。這些技術(shù)包括使用不可變的只讀數(shù)據(jù)、用發(fā)送消息的方式取代更新共享變量,以及在算法中引入新的步驟,以便在一些合適的檢查點(diǎn)(checkpoint)上合并一些可變的本地變量??蓴U(kuò)展性共享技術(shù)可能需要我們修改一些現(xiàn)成的算法??蓴U(kuò)展的數(shù)據(jù)共享需要我們對(duì)算法進(jìn)行一些修改。此外,在面向?qū)ο笤O(shè)計(jì)中,內(nèi)存中往往會(huì)形成一個(gè)復(fù)雜而高度連通的對(duì)象引用圖,這使得傳統(tǒng)的面向?qū)ο竽J胶茈y適用于可擴(kuò)展的并行執(zhí)行要求。對(duì)此,我們首先想到的方法或許是,在確保多任務(wù)共享的情況下,將這些龐大而互相引用的對(duì)象圖成員視為可變的共享狀態(tài),并對(duì)這些成員進(jìn)行外層封裝,用鎖等方式來(lái)強(qiáng)制執(zhí)行串行化訪問(wèn)。遺憾的是,這并不是一種可擴(kuò)展的共享方式。鎖通常會(huì)給所有的內(nèi)核帶來(lái)負(fù)面的性能影響,它強(qiáng)制內(nèi)核暫停和通信的動(dòng)作會(huì)消耗一定的時(shí)間;它在代碼中引入串行部分又會(huì)降低并行化的可能性。而隨著內(nèi)核數(shù)量的增長(zhǎng),鎖帶來(lái)的開(kāi)銷(xiāo)會(huì)使?fàn)幾h愈加強(qiáng)烈。當(dāng)面對(duì)越來(lái)越多任務(wù)需要共享同一塊數(shù)據(jù)的需求時(shí),關(guān)于鎖的成本控制將會(huì)成為程序性能的主要考量。引入同步化機(jī)制(比如鎖)會(huì)降低應(yīng)用程序的可擴(kuò)展性。除了性能問(wèn)題之外,這種依賴于同步機(jī)制的程序還會(huì)有各種各樣的問(wèn)題,其中就包括死鎖(deadlock)。死鎖通常是指在兩個(gè)或者更多的任務(wù)之間發(fā)生的彼此都在等待對(duì)方釋放鎖的狀況。事實(shí)上,大部分關(guān)于并行編程的噩夢(mèng)基本上都起源于錯(cuò)誤地使用了可變共享狀態(tài)(Sharedmutablestate)或者鎖協(xié)議(Lockingprotocol)。盡管如此,如果我們?cè)趯?duì)象圖(objectgraph)中適量地使用一些同步化元素,對(duì)于編寫(xiě)可擴(kuò)展的并行程序是有益的。本書(shū)也會(huì)盡量謹(jǐn)慎地使用同步機(jī)制,你在應(yīng)用中也應(yīng)該慎重。事實(shí)上,你可以把鎖看做并行編程當(dāng)中的goto語(yǔ)句,盡管它很容易出錯(cuò),但在某些特定情況下這是必需的,并且對(duì)于編譯器及其庫(kù)而言,它可以被視為一種最左派(bestleft)\h[1]的選擇。沒(méi)有人會(huì)僅僅因?yàn)樾阅芤蛩鼐蛯⑼綑C(jī)制完全排除在外,除非是正確性需要。畢竟代碼本身的正確性才是最重要的??傊?,在設(shè)計(jì)的過(guò)程中有限地引入同步機(jī)制也是非常重要的設(shè)計(jì)原則。不要事后才想起往應(yīng)用程序里加入同步機(jī)制。\h[1]原文如此,讀者可以理解為最激進(jìn)的做法。作者的具體意圖應(yīng)該是想告訴我們,盡量在自己的程序中少使用這些同步機(jī)制,這樣代碼才具有可讀性,也比較容易維護(hù)。然而和goto語(yǔ)句一樣,有時(shí)候用這種簡(jiǎn)單粗暴的方式也是好處的?!g者注1.2.4設(shè)計(jì)方法通常情況下,程序員會(huì)先對(duì)問(wèn)題所在的問(wèn)題域進(jìn)行甄別,然后在據(jù)此對(duì)代碼進(jìn)行并行化處理,以改善性能,這樣一來(lái),下一次再遇到類似瓶頸時(shí)就可以重用這個(gè)解決方案了。特別是當(dāng)我們需要對(duì)一系列現(xiàn)存的串行程序進(jìn)行并行化處理時(shí),這種思路非常吸引人。然而,盡管這樣做或許確實(shí)能讓程序的性能獲得初步的提升,但正如之前所描述的那樣,這里隱藏著許多陷阱。因此,傳統(tǒng)的分析、優(yōu)化技術(shù)或許并不是最好的選擇。而另一個(gè)更好的選擇是:在充分理解具體問(wèn)題或程序的基礎(chǔ)上,發(fā)掘貫穿整個(gè)應(yīng)用程序的潛在并行化需求。這個(gè)過(guò)程將會(huì)引導(dǎo)你去選擇不同的架構(gòu)或算法,以便更好地拓展程序在并行化方面的潛力。千萬(wàn)不要草率地界定性能瓶頸并貿(mào)然使其并行化,恰恰相反,我們應(yīng)該通過(guò)改變程序的結(jié)構(gòu)來(lái)并行化。在通盤(pán)考慮數(shù)據(jù)結(jié)構(gòu)和算法之前,不要急著界定問(wèn)題的瓶頸所在。分解、協(xié)調(diào)以及可擴(kuò)展性共享技術(shù)之間是密切相關(guān)的,甚至是環(huán)環(huán)相扣的。因此,當(dāng)我們?cè)跒樘囟ǖ膽?yīng)用程序選擇設(shè)計(jì)方案的時(shí)候,應(yīng)該進(jìn)行全方位的思考。請(qǐng)盡量使用設(shè)計(jì)模式。讀完上面這些描述,你或許會(huì)覺(jué)得這太過(guò)形而上學(xué),泛泛而談了。究竟如何將問(wèn)題分解成任務(wù)?到底該采用哪種協(xié)調(diào)技術(shù),有更具體一點(diǎn)的回答嗎?答案是肯定的,這本書(shū)中所描述的設(shè)計(jì)模式就是最好的回答。它們就是解決這些問(wèn)題真正的捷徑。一旦你領(lǐng)會(huì)了這些設(shè)計(jì)模式幕后的設(shè)計(jì)動(dòng)機(jī),就會(huì)自然而然地培養(yǎng)出某種直覺(jué),而這種直覺(jué)將會(huì)幫助你將這些設(shè)計(jì)模式及其變體應(yīng)用到自己的程序中。接下來(lái),我們將會(huì)在剩下的章節(jié)中詳細(xì)地介紹這些模式,以供參考。1.3選擇正確的設(shè)計(jì)模式根據(jù)下面這張表,你可以為自己選擇合適的設(shè)計(jì)模式:此外,你也可以先閱讀每章開(kāi)頭前兩頁(yè)的內(nèi)容,熟悉一下這6個(gè)模式的大致情況,并了解一下它們的基本應(yīng)用場(chǎng)景,然后再更深入地具體研究它們。1.4關(guān)于術(shù)語(yǔ)在一般情況下,我們會(huì)認(rèn)為“并行”和“并發(fā)”是同義詞,但在這本書(shū)中它們是兩個(gè)不同的概念。在本書(shū)中,并發(fā)(concurrency)在更多時(shí)候是一個(gè)與多任務(wù)和異步輸入輸出(I/O)相關(guān)的概念。它實(shí)質(zhì)上是一種執(zhí)行方式,這種執(zhí)行方式能在多線程輪番爭(zhēng)奪時(shí)間片的情況下,確保每一個(gè)線程最終都能獲得時(shí)間片。為了使程序能應(yīng)對(duì)諸如用戶輸入、設(shè)備和傳感器等外部中斷,并發(fā)性是必要的。例如,操作系統(tǒng)和游戲天生就是具有并發(fā)性,即便在單核情況下也是如此。而對(duì)于并行化(parallelism)而言,則更強(qiáng)調(diào)并發(fā)線程可以同時(shí)在多個(gè)處理器內(nèi)核上執(zhí)行的能力。并行編程則側(cè)重于增加我們所占用的處理器資源,并且確保有多個(gè)處理器時(shí)不會(huì)被經(jīng)常中斷,以此來(lái)提升應(yīng)用程序的性能。而且,并發(fā)和并行想要達(dá)到的目標(biāo)也并不相同。并發(fā)的主要目標(biāo)是降低各個(gè)線程長(zhǎng)期不受阻塞占有時(shí)間片的可能性,換句話說(shuō),并發(fā)就是要避免出現(xiàn)有線程被餓死的情況。通常,并發(fā)在操作上往往是必要的。例如,在一個(gè)單核計(jì)算機(jī)的圖形操作系統(tǒng)中,會(huì)有多個(gè)窗口需要同時(shí)刷新顯示區(qū)域,這時(shí)候圖形用戶界面(GUI)就必須支持并發(fā)。相對(duì)而言,而并行化則與吞吐量有關(guān),是一種性能的優(yōu)化,而非功能性需求。它的目標(biāo)是讓處理器的所有內(nèi)核保持最大的有效負(fù)荷。為此,它所使用的調(diào)度算法通常就不是具有搶占性的,正如處理隊(duì)列(queue)或棧(stack)的算法所做的工作一樣。1.5并行的局限根據(jù)阿姆達(dá)爾定律(Amdahl'slaw)\h[1]得出的結(jié)論,并行化所能帶來(lái)的性能改善受到了應(yīng)用程序中串行化處理所占分量的限制。是的,這乍看之下幾乎有點(diǎn)兒違反直覺(jué)。但你并沒(méi)有看錯(cuò)。根據(jù)阿姆達(dá)爾定律的說(shuō)法,無(wú)論我們有多少內(nèi)核可用,程序所能得到的最大速度增長(zhǎng)率也只能符合(1/串行化處理所消耗的時(shí)間)函數(shù)關(guān)系。圖1-1對(duì)此做了說(shuō)明。圖1-1當(dāng)一個(gè)應(yīng)用程序中的串行化處理占25%時(shí)的阿姆達(dá)定律例如,當(dāng)內(nèi)核達(dá)到11個(gè)時(shí),應(yīng)用程序的運(yùn)行速度只比完全串行化運(yùn)行提高了三倍略多一些。即使在更少量?jī)?nèi)核的情況下,運(yùn)行速率也不是線性增長(zhǎng)的,圖1-2對(duì)此做了說(shuō)明。圖1-2一個(gè)擁有25%串行性的程序性能增長(zhǎng)與內(nèi)核數(shù)量增長(zhǎng)之間的關(guān)系如圖1-2所示,隨著內(nèi)核數(shù)量(及整個(gè)應(yīng)用程序的速度)的增加,應(yīng)用程序的串行化部分所耗費(fèi)時(shí)間所占的比例也會(huì)隨即增加(注意,串行化處理時(shí)間本身的值并沒(méi)有受到影響)。這幾乎也說(shuō)明了一個(gè)問(wèn)題,或許我們應(yīng)該對(duì)該程序能在四核計(jì)算機(jī)上得到兩倍的速率增長(zhǎng)感到滿足。更重要的問(wèn)題是,如何才能讓?xiě)?yīng)用程序維持這樣的可擴(kuò)展性,這取決于具體工作本身所擁有的天然串行性(inherentlysequential)時(shí)間。阿姆達(dá)爾定律給予我們的另一個(gè)提示是:或許應(yīng)用程序中確實(shí)有可能會(huì)發(fā)現(xiàn)一些符合并行計(jì)算的特性,以至于我們想利用它為程序添加一些額外的功能。例如,電腦游戲開(kāi)發(fā)員可能會(huì)發(fā)現(xiàn),如果新型多核平臺(tái)的并行硬件的支持,就可以畫(huà)出更為精美復(fù)雜的圖像,即使游戲的邏輯(人工智能引擎)并不適用于并行化執(zhí)行。但是,對(duì)程序整體性能來(lái)說(shuō),最終發(fā)揮影響的可能還是應(yīng)用程序中各功能模塊的具體組合方式。而就具體的實(shí)踐來(lái)說(shuō),我們所能得到的速度增長(zhǎng)率通常會(huì)比阿姆達(dá)爾定律所預(yù)計(jì)的情況還要糟糕一些。因?yàn)殡S著處理器內(nèi)核數(shù)的增加。訪問(wèn)共享內(nèi)存的成本也會(huì)跟著增加。同時(shí),并行算法中包含的協(xié)調(diào)性成本,也不是相應(yīng)串行方案中所必需的。對(duì)此,你可以求助于VisualStudio中的并發(fā)可視化工具,借助其中的分析工具來(lái)了解自己所采用的并行化方案的最終效率??偠灾?,因?yàn)閼?yīng)用程序是由并行化部分和一些必要的串行化部分共同組成的,所以,程序整體性能的增長(zhǎng)率不太可能會(huì)與處理器的內(nèi)核數(shù)形成某種線性關(guān)系,盡管在某些局部,程序確實(shí)能獲得近似的線性增長(zhǎng)。正因?yàn)槿绱耍覀冊(cè)诜治鲂阅苄枨髸r(shí)就有一個(gè)不能忽略的步驟,即我們?cè)诔绦蛑邪l(fā)掘適于并行執(zhí)行的部分,必須建立在充分理解應(yīng)用程序結(jié)構(gòu)及其算法的基礎(chǔ)之上。\h[1]阿姆達(dá)爾定律(Amdahl'slaw):一個(gè)計(jì)算機(jī)科學(xué)界的經(jīng)驗(yàn)法則,因吉恩·阿姆達(dá)爾而得名。它代表了處理器并行運(yùn)算之后效率提升的能力?!g者注1.6一些建議總而言之,我們應(yīng)該盡可能選擇一些最簡(jiǎn)單而有效的方法,這里有一些最基本的建議:·當(dāng)你選擇并行應(yīng)用程序庫(kù)或者架構(gòu)時(shí),應(yīng)該盡可能地使用最高層的抽象邏輯?!ふ?qǐng)盡可能地充分利用好應(yīng)用程序服務(wù)器自身的并行化;例如Web服務(wù)或數(shù)據(jù)庫(kù)本身就具備了一定的并行化。·請(qǐng)使用API來(lái)實(shí)現(xiàn)并行化封裝。例如并行模式庫(kù)。這些程序庫(kù)都是由專家所完成的,都經(jīng)過(guò)了完備的測(cè)試。有了它們的幫助,你可以解決許多常見(jiàn)的并行編程問(wèn)題?!ぎ?dāng)你在構(gòu)思一個(gè)并行化設(shè)計(jì)的時(shí)候,應(yīng)該盡可能全面地考慮整個(gè)應(yīng)用程序的架構(gòu)。一般僅僅關(guān)注提高一些性能熱點(diǎn)和焦點(diǎn)的性能。這種性能也確實(shí)可能得到一些改善,但通常不會(huì)獲得最好的結(jié)果?!ふ?qǐng)盡量使用設(shè)計(jì)模式來(lái)解決問(wèn)題,例如本書(shū)中所介紹的設(shè)計(jì)模式?!ねǔG闆r下,對(duì)算法進(jìn)行重構(gòu)(例如移除一些數(shù)據(jù)共享需求)是一個(gè)更好的選擇,尤其要比你去修改那些原本就為串行化而設(shè)計(jì)的底層代碼要好得多?!こ欠浅1匾?,否則絕不要在并發(fā)性任務(wù)之間共享數(shù)據(jù)。如果非要進(jìn)行共享數(shù)據(jù),請(qǐng)使用API中所提供的容器,例如共享隊(duì)列(sharedqueue)?!こ侨f(wàn)不得已,否則應(yīng)該盡量避免使用類似線程和鎖這樣的低層次元素。同時(shí),要盡可能地將應(yīng)用程序中的抽象級(jí)別從線程提升到任務(wù)。1.7練習(xí)題1)當(dāng)你為問(wèn)題選擇分解粒度(即合并成更大的任務(wù)還是分解成更多的小任務(wù))時(shí),需要權(quán)衡的因素有哪些?2)在內(nèi)核數(shù)由1到4的增加過(guò)程中,如果程序中串行處理所占的時(shí)間約為10%,那么該程序能獲得的最大速度增長(zhǎng)率是多少?3)并行與并發(fā)的不同之處有哪些?1.8更多資源如果希望對(duì)文中所提到的術(shù)語(yǔ)有更好的理解,可以參考本書(shū)最后的術(shù)語(yǔ)表。本書(shū)中所提到的設(shè)計(jì)模式都屬于被業(yè)界用戶廣泛應(yīng)用于開(kāi)發(fā)的并行模式。在這些用戶的概念中,這些模式被看做算法或可實(shí)現(xiàn)的模式。關(guān)于并行模式的分類,我們可以參考Mattson等人的書(shū),還有OurPatternLanguage(OPL)網(wǎng)站,我們盡可能地保證所有的術(shù)語(yǔ)都有據(jù)可查,對(duì)于無(wú)法做到的情況,文中將會(huì)給出相應(yīng)的解釋。對(duì)于MicrosoftWindows平臺(tái)上的并行性,我們也可以在Duffy的書(shū)中找到更詳細(xì)的討論?!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í)行一些具有獨(dú)立性的迭代操作,例如遍歷某集合(collection)中的元素或者執(zhí)行一些限定次數(shù)的迭代。但前提是這些操作是彼此獨(dú)立的,即循環(huán)步驟中不存在對(duì)本地內(nèi)存或磁盤(pán)文件的共同寫(xiě)操作。在語(yǔ)法方面,并行循環(huán)與我們所熟悉的for和for_each并無(wú)多大的區(qū)別,只不過(guò)它們?cè)诙嗪谁h(huán)境中往往會(huì)完成得更快而已。此外,另一個(gè)不同于串行循環(huán)的是,并行循環(huán)的執(zhí)行順序是不確定的,這意味著在并行條件下各步驟的操作通常會(huì)同步進(jìn)行,有時(shí)候,甚至兩個(gè)步驟的操作順序會(huì)與它們?cè)诖醒h(huán)中的情況完全相反。這里唯一能保證的是:當(dāng)循環(huán)完成時(shí)其中所有的步驟都會(huì)被執(zhí)行到。我們可以輕而易舉地將一個(gè)串行循環(huán)(sequentialloop)改為并行循環(huán)(parallelloop)。但是,你可千萬(wàn)不能就此認(rèn)為簡(jiǎn)單就不會(huì)出錯(cuò)。畢竟,判斷循環(huán)步驟之間是否具有獨(dú)立性是一件非常困難的事。這需要在具體的實(shí)踐中不斷地學(xué)習(xí)并總結(jié)經(jīng)驗(yàn)。有時(shí)候,如果我們不幸在一個(gè)具有某種依賴關(guān)系的循環(huán)中誤用了并行循環(huán)模式,很可能就會(huì)導(dǎo)致某些不可預(yù)測(cè)的行為,程序有可能會(huì)就此停止響應(yīng),有時(shí)甚至還會(huì)出現(xiàn)那種運(yùn)行100萬(wàn)次才出現(xiàn)一次的詭異bug。總而言之,“獨(dú)立性”是我們能否使用并行循環(huán)模式的關(guān)鍵,而這也是本章接下來(lái)要介紹的重點(diǎn)內(nèi)容。并行循環(huán)模式一般用于處理基于多組數(shù)據(jù)的獨(dú)立性操作。同時(shí),這也是數(shù)據(jù)并行化(dataparallelism)技術(shù)的一個(gè)標(biāo)準(zhǔn)應(yīng)用。對(duì)于并行循環(huán)來(lái)說(shuō),決定它并行度的通常不是代碼,而是運(yùn)行時(shí)環(huán)境(run-timeenvironment)。也就是說(shuō),它取決于運(yùn)行時(shí)環(huán)境能同時(shí)調(diào)用多少內(nèi)核來(lái)完成這些循環(huán)步驟。然而,無(wú)論我們有多少內(nèi)核可用,循環(huán)都應(yīng)該能夠始終正確地運(yùn)行。即使在單核處理器上,只要該循環(huán)的每次迭代操作所執(zhí)行的工作量不是太小,它的性能就理應(yīng)與相應(yīng)的串行代碼相差無(wú)幾(也許只相差幾個(gè)百分點(diǎn));一旦有了多核處理器,性能就會(huì)立即得到改善。畢竟在多數(shù)情況下,性能與內(nèi)核數(shù)之間還是會(huì)存在著一定的比例關(guān)系的。2.1基本用法在并行模式庫(kù)(PPL)中,我們已經(jīng)為你內(nèi)置了與for、for_each對(duì)應(yīng)的并行化函數(shù)。其中,parallel_for函數(shù)可以被用來(lái)遍歷整數(shù)區(qū)間,而parallel_for_each函數(shù)則可以用于遍歷用戶所提供的數(shù)據(jù)。如果你想讓帶有獨(dú)立性迭代的for和for_each循環(huán)在多核計(jì)算機(jī)上運(yùn)行得更快,就應(yīng)該去使用它們的并行版本。2.1.1并行版的for循環(huán)現(xiàn)在,讓我們來(lái)看一段標(biāo)準(zhǔ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);}接下來(lái),為了體現(xiàn)出多核的性能優(yōu)勢(shì),我們改用parallel_for函數(shù)來(lái)代替for關(guān)鍵字,并用lambda表達(dá)式(lambdaexpression)\h[1]來(lái)替代原來(lái)的循環(huán)體。如果你想使用并行循環(huán),不要忘記循環(huán)各步驟間需要彼此獨(dú)立,確保它們之間的通信不以寫(xiě)共享變量的方式來(lái)實(shí)現(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ù)就能利用多核資源來(lái)遍歷上述代碼中的索引區(qū)間。在代碼中,形如[capturedvariables](args){body}的語(yǔ)句實(shí)質(zhì)上是一個(gè)lambda表達(dá)式。在繼續(xù)深入閱讀之前,你或許應(yīng)該去復(fù)習(xí)一下C++中關(guān)于lambda表達(dá)式的語(yǔ)法內(nèi)容。另外,parallel_for函數(shù)還有一系列不同的重載(overload)版本。上述示例中所調(diào)用的版本聲明如下。template<typename_Index_type,typename_Function>voidparallel_for(_Index_type_First,_Index_type_Last,const_Function&_Func);在該函數(shù)中,前兩個(gè)參數(shù)所表示的是該迭代的邊界。第一個(gè)參數(shù)是循環(huán)的下界位;第二個(gè)參數(shù)則為循環(huán)的出界位,即上界位+1;而第三個(gè)參數(shù)則是一個(gè)作用于歷次迭代的函數(shù),迭代次數(shù)是該函數(shù)的參數(shù)之一。這意味著該循環(huán)的每次迭代操作都會(huì)調(diào)用這個(gè)函數(shù)。關(guān)于parallel_for函數(shù)的另一個(gè)重載,我們將會(huì)在2.3節(jié)中再介紹。和串行循環(huán)不同的是,parallel_for方法不能保證操作的順序,即對(duì)較大索引值的操作可能排在較小索引值之前。例子中類似于parallel_for第三參數(shù)那樣的,形如[capturedvariables](args){body}的表達(dá)式,被稱為lambda表達(dá)式。該表達(dá)式實(shí)質(zhì)是一個(gè)能夠捕獲自己所在作用域中變量的函數(shù)對(duì)象。當(dāng)然,_Func參數(shù)并不一定非得是lambda表達(dá)式,它也可以是一個(gè)函數(shù)指針\h[2](指向一個(gè)在別處聲明的函數(shù))。如果你對(duì)lambda表達(dá)式的語(yǔ)法還不熟悉,可以參考2.7節(jié)中所推薦的文章。相信我,一旦你使用了lambda表達(dá)式,就會(huì)對(duì)它有一種相見(jiàn)恨晚的感覺(jué)。\h[1]lambda表達(dá)式是C++11標(biāo)準(zhǔn)(有時(shí)候也叫C++0x)中的新特性,不熟悉的讀者可暫時(shí)將其理解為一個(gè)即寫(xiě)即用的匿名函數(shù)?!g者注\h[2]此處的_Func在代碼中是個(gè)引用(reference)參數(shù),或許會(huì)引起讀者的困惑。而實(shí)質(zhì)上作者是指_Function模板參數(shù)也可以推導(dǎo)為一個(gè)指針(pointer)類型,與_Func參數(shù)本身是_Function&類型并不矛盾?!g者注2.1.2parallel_for_each下面是一個(gè)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)勢(shì),我們用parallel_for_each函數(shù)來(lái)替代for_each關(guān)鍵字\h[1]。parallel_for_each會(huì)在循環(huán)體內(nèi)處理容器中的每一個(gè)元素。vector<size_t>inputs=……intworkload=……parallel_for_each(inputs.cbegin(),inputs.cend(),[workLoad](size_ti){DoWork(i,workLoad);});在語(yǔ)法上,parallel_for_each函數(shù)與std::for_each基本相同。第一個(gè)參數(shù)是一個(gè)迭代器(iterator),指向操作區(qū)間內(nèi)的第一個(gè)元素;第二個(gè)參數(shù)迭代器則指向該區(qū)間內(nèi)最后一個(gè)元素的后一個(gè)位置;第三個(gè)參數(shù)則是一個(gè)作用于該區(qū)間內(nèi)每一個(gè)元素的函數(shù)對(duì)象。不要忘記,由于各迭代操作之間必須保持彼此獨(dú)立,因此,每一個(gè)循環(huán)體只能更新那些傳遞給它的字段實(shí)體。由于parallel_for_each方法的執(zhí)行順序是不確定的(這與for_each串行循環(huán)不同),因此這里的輸入?yún)?shù)并非總能按照既定的順序來(lái)處理。\h[1]原文如此,而實(shí)際上兩者都是函數(shù),并不存在關(guān)鍵字(keyword)。——譯者注2.1.3期望為何默認(rèn)情況下,循環(huán)的并行度(即硬件能同時(shí)運(yùn)行多少次迭代)取決于處理器中可用的內(nèi)核數(shù)量??捎玫膬?nèi)核越多循環(huán)便運(yùn)行得越快,這種趨勢(shì)可以一直持續(xù)到我們遇上阿姆達(dá)定律所預(yù)言的那個(gè)收益遞減點(diǎn)。從那之后,循環(huán)究竟能運(yùn)行得多快就取決于該循環(huán)本身的工作內(nèi)容了(請(qǐng)參閱第1章關(guān)于阿姆達(dá)定律的討論)。內(nèi)核的增加能使循環(huán)運(yùn)行得更快,但這也總會(huì)有個(gè)上限。為循環(huán)體選擇正確的粒度是很有必要的,因?yàn)橛刑噙^(guò)小的并行循環(huán)因其工作量過(guò)度分解而導(dǎo)致其多核優(yōu)勢(shì)被循環(huán)本身的開(kāi)銷(xiāo)抵消殆盡。在parallel_for或parallel_for_each的執(zhí)行過(guò)程中,如果某個(gè)迭代操作拋出了異常(exception),該異常將會(huì)被上拋至該函數(shù)所調(diào)用的線程處進(jìn)行處理。在2.3節(jié)中,我們將會(huì)更詳細(xì)地講解并行循環(huán)中的異常處理問(wèn)題。強(qiáng)健的異常處理機(jī)制是并行循環(huán)處理的重要組成部分之一。在用并行循環(huán)替換了串行循環(huán)之后,如果你發(fā)現(xiàn)程序不能按照自己所預(yù)期的方式執(zhí)行,那么,最大的可能就是該循環(huán)步驟之間并非是彼此獨(dú)立的。下面,我們將會(huì)介紹一些常見(jiàn)的依賴性循環(huán)體(dependentloopbody)\h[1],以你供參考:請(qǐng)仔細(xì)檢查循環(huán)迭代之間是否存在某種依賴關(guān)系。因?yàn)榈侥壳盀橹?,?duì)這種依賴性的疏忽是我們?cè)谑褂貌⑿醒h(huán)時(shí)最常見(jiàn)錯(cuò)誤。·對(duì)共享變量執(zhí)行寫(xiě)操作:如果循環(huán)體內(nèi)對(duì)共享變量執(zhí)行了寫(xiě)操作,那么該循環(huán)體就是具有依賴性的。這在我們需要執(zhí)行某些聚合求值操作時(shí)很常見(jiàn)。下面這個(gè)示例中的total就是被所有迭代體共同操作的變量。for(inti=1;i<n;i++)total+=data[i];如果真的遇上了類似的情況,建議你參考第4章中的并行聚合模式。當(dāng)然,共享變量的形式是多樣化的。任何在循環(huán)體外部聲明的變量都有可能是共享變量。例如,如果我們對(duì)一些類似于類或數(shù)組(array)這樣的類型對(duì)象使用某種共享性引用的話,這些對(duì)象中的所有字段、元素都有可能會(huì)被隱式地轉(zhuǎn)換為共享變量;另外,通過(guò)引用或指針?lè)绞絺鬟f的函數(shù)參數(shù)也會(huì)導(dǎo)致變量共享;同樣,在lambda表達(dá)式中使用引用方式捕捉的變量也一樣會(huì)如此?!な褂脤?duì)象模型的數(shù)據(jù)訪問(wèn)器(dataaccessor):如果循環(huán)體是通過(guò)數(shù)據(jù)訪問(wèn)器來(lái)處理一個(gè)對(duì)象的話,我們就有必要了解一下這些訪問(wèn)器,看看其中是否涉及了某種共享狀態(tài)抑或只是對(duì)象自身的狀態(tài)。例如,下面有個(gè)名為GetParent的訪問(wèn)器方法很可能就涉及了某種全局狀態(tài)(globalstate):在用訪問(wèn)器讀取數(shù)據(jù)的時(shí)候,你必須非常謹(jǐn)慎。因?yàn)檫@些大型對(duì)象往往會(huì)以令人費(fèi)解的方式共享它們的可變狀態(tài)。for(inti=0;i<n;i++)SomeObject[i].GetParent().Update();這個(gè)例子中的循環(huán)迭代體就很有可能不具備獨(dú)立性。因?yàn)閷?duì)于該循環(huán)中的每一個(gè)i來(lái)說(shuō),SomeObject[i].GetParent()引用的都有可能是同一個(gè)對(duì)象實(shí)體。·引用了非線程安全的數(shù)據(jù)類型和函數(shù):如果在一個(gè)并行循環(huán)中,循環(huán)體所使用的數(shù)據(jù)類型或函數(shù)不是線程安全的話,就表示這些線程中已經(jīng)存在著依賴關(guān)系了,因此我們也就可以斷定該循環(huán)體中的操作是不具有獨(dú)立性的?!ぱh(huán)執(zhí)行性依賴(Loop-carrieddependence):如果在parallell_for循環(huán)中,循環(huán)體中所執(zhí)行的數(shù)值運(yùn)算是基于循環(huán)索引變量的,那么出現(xiàn)循環(huán)執(zhí)行性依賴的可能性就非常大了。例如在下面的代碼中,循環(huán)體中同時(shí)引用了data[i]和data[i-1],一旦這樣的代碼出現(xiàn)在parallel_for中,我們是無(wú)法確保data[i-1]能在處理data[i]之前就被更新完成的?;谘h(huán)索引變量的數(shù)值運(yùn)算,特別是加減法操作,通常都會(huì)導(dǎo)致循環(huán)執(zhí)行性依賴。for(inti=1;i<N;i++)data[i]=data[i]+data[i-1];然而在某些時(shí)候,這種包含循環(huán)執(zhí)行性依賴的并行算法也是可行的,但這已經(jīng)超出了本書(shū)的討論范圍。對(duì)此,我們最好的建議是:要么考慮一下程序中別的并行化機(jī)會(huì),要么分析一下你的算法能否匹配某些用于科學(xué)計(jì)算的高級(jí)并行模式,例如并行掃描和并行動(dòng)態(tài)規(guī)劃之類的。當(dāng)我們?cè)趯ふ也⑿谢瘷C(jī)會(huì)的時(shí)候,對(duì)應(yīng)用程序的性能進(jìn)行詳細(xì)分析,將會(huì)有助于我們了解程序的運(yùn)行時(shí)間主要消耗在哪些地方。但是,這種分析終究不能代替你理解應(yīng)用程序的算法和結(jié)構(gòu),例如,它不能替你判斷循環(huán)體中是否存在依賴關(guān)系。不要對(duì)代碼的性能分析寄予過(guò)高的期望——它不能替代你分析算法,所以我們只能靠自己。\h[1]此處必然是循環(huán)體(loopbody)而非循環(huán)(loop),因?yàn)檠h(huán)的索引變量本身也是一種共享變量,而且循環(huán)必然要對(duì)它執(zhí)行讀寫(xiě)操作,這種讀寫(xiě)操作不會(huì)影響循環(huán)體的獨(dú)立性?!g者注2.2實(shí)例示范接下來(lái),我們將通過(guò)一個(gè)具體的實(shí)例向你演示并行循環(huán)模式的運(yùn)用。Fabrikam航運(yùn)公司為自己的商業(yè)用戶增設(shè)了一項(xiàng)借貸業(yè)務(wù),他們根據(jù)用戶的信用度來(lái)鑒別哪些賬戶可能存在風(fēng)險(xiǎn)。并且在每個(gè)賬戶內(nèi)都包含了一份結(jié)欠記錄。因?yàn)镕abrikam公司從歷史記錄中發(fā)現(xiàn)了一個(gè)規(guī)律:在賬戶被判違約之前的數(shù)月內(nèi),那些違約客戶的借貸余額都會(huì)呈現(xiàn)出穩(wěn)定的增長(zhǎng)趨勢(shì)。于是,為了鑒別風(fēng)險(xiǎn)賬戶,F(xiàn)abrikam公司使用了統(tǒng)計(jì)學(xué)中的趨勢(shì)分析法,據(jù)此估算出每個(gè)賬戶所能達(dá)到的最高負(fù)債額。如果分析程序預(yù)測(cè)某用戶的賬戶將在三個(gè)月內(nèi)超出他的負(fù)債限額,該賬戶就會(huì)被標(biāo)記下來(lái),并將面臨Fabrikam公司信用分析師的人工審查。在這個(gè)應(yīng)用中,頂層循環(huán)將會(huì)遍歷賬戶庫(kù)中的所有用戶。循環(huán)體會(huì)根據(jù)用戶的負(fù)債記錄制定出一條趨勢(shì)線,據(jù)此推定其負(fù)債范圍,然后用它來(lái)比對(duì)之前設(shè)定的負(fù)債限額。一旦發(fā)現(xiàn)該用戶有超額負(fù)載的可能性,程序就會(huì)為該賬戶分配警告標(biāo)志。這個(gè)應(yīng)用程序的一個(gè)重要特點(diǎn)是,每個(gè)用戶的信用度是可以被獨(dú)立估算的。這意味著一個(gè)用戶的信用度并不依賴于另一個(gè)用戶,這些操作是彼此獨(dú)立執(zhí)行的。因此,只要用并行循環(huán)替換掉其中的for_each串行循環(huán),我們的信用分析程序就能運(yùn)行得更快一些。關(guā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();});}在這個(gè)程序中,UpdatePredictionsSequential方法被用來(lái)處理賬戶庫(kù)中的每一個(gè)賬戶。而其中的Fit方法是一個(gè)工具函數(shù),它通過(guò)統(tǒng)計(jì)學(xué)中的最小二乘法根據(jù)一組(用戶)數(shù)據(jù)繪制出趨勢(shì)線。此外,F(xiàn)it方法是一個(gè)純函數(shù)過(guò)程即它不會(huì)改變?nèi)魏螙|西的狀態(tài)。\h[1]該值是根據(jù)趨勢(shì)得出的三個(gè)月內(nèi)的預(yù)測(cè)。如果預(yù)測(cè)值越過(guò)了負(fù)債限制(賬戶系統(tǒng)中的負(fù)債額是個(gè)負(fù)值),該賬戶就會(huì)被標(biāo)志為審查狀態(tài)。\h[1]作者是在強(qiáng)調(diào)這個(gè)函數(shù)不會(huì)引入任何依賴關(guān)系?!g者注2.2.2parallel_for_each版的CreditReview并行版的信用度分析程序與串行版本幾乎相差無(wú)幾: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();});}如你所見(jiàn),除了用parallel_for_each替代for_each外,UpdatePredictionsParallel方法的代碼與UpdatePredictionsSequential幾乎完全一致。2.2.3性能對(duì)比根據(jù)在一臺(tái)四核計(jì)算機(jī)上運(yùn)行CreditReview的情況來(lái)看,parall_for_each版本相比于其串行版本快了近四倍。至于具體的時(shí)間變化情況,建議讀者在自己的計(jì)算機(jī)上運(yùn)行一下網(wǎng)上所提供代碼來(lái)獲得第一手的數(shù)據(jù)。2.3模式變體上面的信用分析實(shí)例為我們展示了一個(gè)并行循環(huán)的典型用法,但它還可以有很多變體用法。本節(jié)將介紹其中最重要的幾個(gè)。盡管這些變體被使用的機(jī)會(huì)并不會(huì)太多,但是你應(yīng)該知道它們是可行的。2.3.1提前退出循環(huán)在串行迭代中,退出循環(huán)的操作并不會(huì)令人感到陌生,而這類操作在并行循環(huán)中并不常見(jiàn),但有時(shí)我們依然會(huì)用得著它。下面是一個(gè)串行情況下的做法:intn=……for(inti=0;i<n;i++){//……if(/·stoppingconditionistrue*/)break;}相比較而言,并行循環(huán)的情況會(huì)更復(fù)雜一些。因?yàn)槠渲型鶗?huì)有不止一個(gè)步驟在同時(shí)運(yùn)行,而且各步驟的執(zhí)行順序完全無(wú)法預(yù)知。但是,我們倒是可以通過(guò)取消任務(wù)組的方式來(lái)實(shí)現(xiàn)從一個(gè)并行循環(huán)中提前退出的效果。關(guān)于任務(wù)組,我們將會(huì)在第3章中詳細(xì)介紹。使用task_group::cancel方法可以幫助你提前退出一個(gè)并行循環(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);});});如你所見(jiàn),如果想要提前退出一個(gè)并行循環(huán),我們需要先為該循環(huán)建立一個(gè)任務(wù)組對(duì)象,然后讓該循環(huán)在任務(wù)組內(nèi)執(zhí)行。這時(shí)候,如果你想退出循環(huán),只需要調(diào)用該任務(wù)組的cancel方法即可。不要忘了并行循環(huán)是無(wú)序執(zhí)行的,所以取消一個(gè)并行循環(huán)時(shí)并不能確保位置更靠后的迭代操作沒(méi)有先被運(yùn)行。但是和串行循環(huán)不一樣的是,我們應(yīng)該始終牢記并行循環(huán)中的步驟是無(wú)序執(zhí)行的。因此當(dāng)你取消一個(gè)并行循環(huán)任務(wù)組的時(shí)候,并不能保證沒(méi)有更靠后的迭代在cancel被執(zhí)行之前已經(jīng)運(yùn)行完成。2.3.2異常處理當(dāng)一個(gè)并行循環(huán)中拋出未處理異常時(shí),就不會(huì)再執(zhí)行新的步驟了。但在默認(rèn)情況下,異常拋出時(shí)正在運(yùn)行的其他迭代操作仍將繼續(xù)。一直到它們?nèi)客瓿芍螅摬⑿醒h(huán)才會(huì)將異常上拋到調(diào)用它的線程中去。拋出一個(gè)未處理異常可以阻止新的迭代操作開(kāi)始。由于循環(huán)運(yùn)行在并行環(huán)境中,所以可能會(huì)發(fā)生不止一個(gè)異常。如果遇上這種情況,并行循環(huán)會(huì)隨機(jī)拋出一個(gè)異常,而剩下的異常將不會(huì)被外界所察覺(jué)\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]即無(wú)論并行循環(huán)中有多少異常發(fā)生,在循環(huán)所在線程中能看到的只有一個(gè),而且是隨機(jī)的?!g者注2.3.3小型循環(huán)體的特殊處理如果循環(huán)體中只執(zhí)行了少量的工作,我們或許可以將這些迭代分配到更大的工作單元中去,以謀求更好的性能。這樣做是有原因的。當(dāng)執(zhí)行一個(gè)循環(huán)時(shí)通常會(huì)引入兩種類型的成本:管理工作線程的成本和調(diào)用函數(shù)對(duì)象的成本。大多數(shù)情況下這些成本是微不足道的,但是,當(dāng)循環(huán)體很小時(shí)這些開(kāi)銷(xiāo)就不能忽略不計(jì)了。如果你有很多迭代,而每次迭代的工作量又很小的話,請(qǐng)考慮使用分組策略。parallel_for的其中一個(gè)重載版本允許我們指定索引值的步長(zhǎng)(stepsize),當(dāng)我們使用步長(zhǎng)大于1的迭代時(shí),就可以在并行循環(huán)中內(nèi)嵌一個(gè)串行循環(huán)。這樣一來(lái),外(并行)循環(huán)的每一次迭代就由對(duì)逐個(gè)索引值進(jìn)行處理轉(zhuǎn)變?yōu)閷?duì)一個(gè)索引值范圍進(jìn)行處理。這種將迭代分組的方法可以讓我們避免一些并行循環(huán)固有的開(kāi)銷(xiāo)。具體的實(shí)例代碼如下: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);});相對(duì)于普通無(wú)分組的parallel_for函數(shù)來(lái)說(shuō),這種將數(shù)據(jù)分組的方式會(huì)導(dǎo)致更為復(fù)雜應(yīng)用程序的邏輯。一旦遇上大工作量的迭代(或者當(dāng)?shù)鷧^(qū)間的大小不均時(shí)),這個(gè)方法或許就無(wú)法幫助我們獲得較好的性能了。所以通常只有在經(jīng)過(guò)了深思熟慮之后,或者是在這種循環(huán)體極小而迭代次數(shù)很多的特定情況下,我們才會(huì)考慮采用這種更為復(fù)雜的語(yǔ)法。至于分組的數(shù)量,則一般取決于計(jì)算機(jī)上可用的內(nèi)核數(shù)。默認(rèn)條件下,分組數(shù)量大約維持在內(nèi)核數(shù)的3到10倍是比較理想的。除此之外,另一種處理這種小型循環(huán)體的方法是,調(diào)用parallel_for_fixed和parallel_for_each_fixed。這兩個(gè)函數(shù)可以在并發(fā)運(yùn)行時(shí)相關(guān)的示例程序包中找到。默認(rèn)情況下,parallel_for和parallel_for_each都具有動(dòng)態(tài)負(fù)載平衡(dynamicloadbalancing)的功能。一旦循環(huán)體中的工作量過(guò)小,這種負(fù)載平衡所帶來(lái)的開(kāi)銷(xiāo)會(huì)對(duì)程序產(chǎn)生很大的影響。該示例包中的parallel_for_fixed和parallel_for_each_fixed則不會(huì)執(zhí)行這樣的負(fù)載平衡,因此它們?cè)谛⊙h(huán)體中或許能比parallel_for和parallel_for_each運(yùn)行得更快一些。這兩組函數(shù)的另一個(gè)不同之處是,parallel_for和parallel_for_each會(huì)檢查每一次迭代的注銷(xiāo)情況,相比之下,parallel_for_fixed和parallel_for_each_fixed并不會(huì)在它們的子區(qū)間內(nèi)執(zhí)行這種檢查。2.3.4并行度控制一般情況下,并行循環(huán)的迭代操作在內(nèi)核上的具體分配是由系統(tǒng)自行管理的。但在某些特殊的時(shí)候,我們也需要取得更多的控制權(quán)。在并行循環(huán)模式中,我們經(jīng)常會(huì)在各種情況下用到這些模式變體。例如,為了模擬一些性能較差的硬件,我們需要通過(guò)降低程序的并行度來(lái)執(zhí)行一些性能測(cè)試;而如果循環(huán)中各次迭代都要花費(fèi)大量時(shí)間來(lái)等待I/O輸入,就適于提高程序的并行度來(lái)提高執(zhí)行線程的數(shù)量,使之大于內(nèi)核數(shù)。你可以控制一個(gè)并行循環(huán)中并發(fā)的活動(dòng)線量的最大數(shù)量。所謂并行度,實(shí)質(zhì)上是指我們?cè)诘鷷r(shí)占用的內(nèi)核數(shù)量。一般來(lái)說(shuō),并行度都是交由系統(tǒng)的底層組件自行管理的,例如parallel_for和parallel_for_each的函數(shù)實(shí)現(xiàn)、并發(fā)運(yùn)行時(shí)的任務(wù)調(diào)度,以及操作系統(tǒng)中的線程調(diào)度等。這些組件都為各種條件下優(yōu)化系統(tǒng)的吞吐量做出了一定的貢獻(xiàn)。因此,盡管無(wú)法直接控制并行度,但我們可以通過(guò)控制執(zhí)行一個(gè)并行循環(huán)的線程數(shù)量來(lái)對(duì)其施加影響。而這需要我們?nèi)ピO(shè)置一下SchedulerPolicy類中MinConcurrency和MaxConcurrency策略。關(guān)于設(shè)置這些策略(policies)的更多信息,讀者可以參考附錄A中的相關(guān)內(nèi)容。2.4反面模式所謂反面模式(Anti-Patterns),通常指的是一些具有警示性的反面故事,它為我們凸顯出了一些需要進(jìn)行深思熟慮的事件和問(wèn)題域。接下來(lái),我們將會(huì)為你列舉一些在使用并行循環(huán)時(shí)需要注意的問(wèn)題。2.4.1隱性循環(huán)體依賴錯(cuò)誤分析了循環(huán)中的依賴關(guān)系往往是導(dǎo)致軟件缺陷的主要根源之一。因此,我們要留意所有并行循環(huán)體中不可見(jiàn)的隱性依賴,這些是非常容易出錯(cuò)的地方。舉例來(lái)說(shuō),如果在非線程安全的情況下,我們?cè)诟鱾€(gè)并行迭代操作之間共享了一個(gè)類實(shí)例的話,就很可能會(huì)遇上這種微妙的依賴性。同樣,你也要留意在lambda表達(dá)式的封閉域中使用引用變量來(lái)共享狀態(tài)的問(wèn)題。即使各循環(huán)體之間彼此并不完全獨(dú)立,我們依然是可以使用并行循環(huán)的。只不過(guò),在這種情況下,我們必須確保所有的共享變量是受保護(hù)的,并且是可以同步的。同時(shí),我們還必須對(duì)自己所引入的任何同步化機(jī)制在性能影響方面有充分的認(rèn)知。盡管這種機(jī)制的引入可能會(huì)極大地降低并發(fā)程序的性能,但如果你因此而忽略了它的必要性,同樣會(huì)導(dǎo)致程序中出現(xiàn)災(zāi)難性的并且難以重現(xiàn)的bug。如果循環(huán)體之間并不存在任何依賴關(guān)系——例如執(zhí)行求和運(yùn)算——你或許就可以考慮我們將在第4章中介紹的并行循環(huán)變體。2.4.2少量迭代的小循環(huán)體如果我們只是在很小的并行循環(huán)體中處理一些很有限的數(shù)據(jù)元素,性能或許并不會(huì)得到多少改善,這種情況下并行循環(huán)本身的成本反而會(huì)成為主要開(kāi)銷(xiāo)。顯然,如果僅僅是簡(jiǎn)單地把每個(gè)串行的for循環(huán)改為parallel_for循環(huán),并不一定就能獲得好的結(jié)果。2.4.3重復(fù)輸入性枚舉在使用parallel_for_each的過(guò)程中,如果輸入中有多個(gè)指針或引用變量指向了同一個(gè)對(duì)象實(shí)體,往往就意味著我們的代碼中可能存在不安全競(jìng)爭(zhēng)的情況。因?yàn)槿绻粋€(gè)對(duì)象引用(objectreference,即指向同一個(gè)類實(shí)例的指針或引用)在循環(huán)輸入中不止一次地出現(xiàn),就很可能會(huì)產(chǎn)生兩個(gè)并行線程同時(shí)更新一個(gè)對(duì)象的問(wèn)題。請(qǐng)不要在并行循環(huán)中反復(fù)使用同一個(gè)實(shí)體的副本,如果有一個(gè)對(duì)象不止一次地出現(xiàn)在某個(gè)循環(huán)的輸入中,該對(duì)象就很有可能會(huì)被兩個(gè)并行線程同時(shí)更新。2.4.4基于協(xié)同性阻塞的交叉調(diào)度在一個(gè)并行循環(huán)中,如果它的每次迭代都要執(zhí)行協(xié)同性阻塞(cooperativeblocking)\h[1],就很可能會(huì)使任務(wù)調(diào)度器中所產(chǎn)生的線程數(shù)量多得超乎想象。因此,我們應(yīng)該盡可能少地在此類循環(huán)中使用協(xié)同性阻塞操作。\h[1]關(guān)于協(xié)同性阻塞的內(nèi)容請(qǐng)參考第3章中的內(nèi)容?!g者注2.5相關(guān)模式并行循環(huán)其實(shí)還是并行聚合模式的基礎(chǔ)模式。而后者正是我們將要在第4章中介紹的內(nèi)容。2.6練習(xí)題1)在下面的這些問(wèn)題中,有哪一個(gè)能適用于本章所介紹的并行循環(huán)技術(shù)?a)對(duì)內(nèi)存中的一個(gè)擁有100萬(wàn)個(gè)數(shù)字元素的數(shù)組進(jìn)行排序。b)讀取一個(gè)文本文件,并按字母順序輸出每一行的單詞。c)對(duì)一個(gè)集合中的所有數(shù)字求和,并輸出最終的運(yùn)算結(jié)果。d)將兩個(gè)集合中的數(shù)字兩兩相加,并將每對(duì)數(shù)字的相加值輸出到第三個(gè)集合中。e)對(duì)一個(gè)文本文件集合中出現(xiàn)的每個(gè)單詞執(zhí)行統(tǒng)計(jì)計(jì)數(shù)。f)找出一個(gè)文本文件集合中出現(xiàn)頻率最高的單詞。2)從上題中選擇一個(gè)你認(rèn)為合適的問(wèn)題,分別用串行循環(huán)和并行循環(huán)實(shí)現(xiàn)一個(gè)解決方案。3)對(duì)CodePlex站點(diǎn)中的CreditReview實(shí)例執(zhí)行性能分析。用命令行選項(xiàng)獨(dú)立地變換迭代次數(shù)(即賬戶數(shù))和每個(gè)迭代的工作量(即信用記錄中的月份數(shù))。并記錄下該程序三個(gè)版本所有的執(zhí)行時(shí)間報(bào)告。然后,變換賬戶數(shù)和月份數(shù)的各種不同組合在不同內(nèi)核數(shù),以及不同執(zhí)行環(huán)境(從其他應(yīng)用程序中載入)的各種計(jì)算機(jī)上進(jìn)行反復(fù)測(cè)試。2.7補(bǔ)充閱讀在這本書(shū)中,幾乎所有的實(shí)例都運(yùn)用了MicrosoftVisualC++中的功能特性和程序庫(kù),因此關(guān)于這些功能、程序庫(kù)以及l(fā)ambda表達(dá)式的詳細(xì)資料,MSDN自然是首要的參考來(lái)源。此外,Mattson等人也在自己的書(shū)中介紹了一些關(guān)于并行編程的軟件設(shè)計(jì)模式,值得一提的是,這些模式并不局限于特定的語(yǔ)言和程序庫(kù)。最后,Messmer的文章也值得你看看,其中介紹了幾個(gè)與本章相關(guān)的模式,以及一些關(guān)于如何在PPL中使用并行循環(huán)的建議。·Mattson,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章并行任務(wù)并行任務(wù)是一種可以讓我們?cè)谕粫r(shí)間內(nèi)執(zhí)行的異步操作(asynchronousoperations)。這種方法也被稱為任務(wù)并行化。第2章所介紹的并行循環(huán)模式,實(shí)質(zhì)上是一種實(shí)現(xiàn)用單一操作處理多組數(shù)據(jù)的并行技術(shù),有時(shí)也稱為數(shù)據(jù)并行化(dataparallelism)。第3章將要為你介紹的是,面對(duì)多個(gè)同時(shí)運(yùn)行的異步操作時(shí)所應(yīng)該選擇的處理技術(shù)。在這種情況下,如果我們能實(shí)現(xiàn)任務(wù)的并行化執(zhí)行,就可以在程序控制流中實(shí)現(xiàn)類似于fork\h[1]這樣的操作。這種做法通常也稱為任務(wù)并行化(taskparallelism)。正因?yàn)槿绱耍⑿腥蝿?wù)模式有時(shí)候也會(huì)稱為Fork/Join模式或者M(jìn)aster/Worker模式。數(shù)據(jù)并行化和任務(wù)并行化就好像位于我們技術(shù)光譜的兩端\h[2]。前者通常側(cè)重于處理單一操作的多重輸入;而后者則往往側(cè)重于處理多個(gè)操作,并且每個(gè)操作中都有屬于自己的輸入。在并行模式庫(kù)(PPL)中,我們一般會(huì)通過(guò)task_group類的方法來(lái)啟動(dòng)并管理任務(wù)。你可以在ppl.h頭文件中找到task_group類的相關(guān)聲明。在該類中,run方法主要用于創(chuàng)建新的任務(wù),并對(duì)其進(jìn)行調(diào)度;而wait方法則主要用于使程序暫停下來(lái),以等待某個(gè)任務(wù)組對(duì)象完成它所有的任務(wù)。從fork/join組操作角度來(lái)看,這里的run方法就相當(dāng)于fork操作,而wait方法則基本等同于join。在PPL中,并行任務(wù)是通過(guò)task_group類來(lái)管理的。任務(wù)的調(diào)度是并行任務(wù)模式中的一個(gè)重點(diǎn)內(nèi)容。這與線程不同,新任務(wù)啟動(dòng)之后通常并不會(huì)立即執(zhí)行。而是先被移到一個(gè)工作隊(duì)列中。等處理器有空閑資源時(shí),再由相關(guān)的調(diào)度器將它們從工作隊(duì)列中調(diào)出來(lái)運(yùn)行。因此,只要我們確保任務(wù)足夠多,并且這些任務(wù)之間不存在序列化依賴關(guān)系(serializingdependency),程序性能往往就能隨著可用內(nèi)核數(shù)的增多而改善。在這里,我們又有了一次體會(huì)潛在并行化概念的機(jī)會(huì)。還記得嗎?這個(gè)概念在第一章中曾被我們反復(fù)提及。此外,對(duì)基于任務(wù)模式的應(yīng)用程序來(lái)說(shuō),另一個(gè)重點(diǎn)內(nèi)容就是異常處理機(jī)制。在PPL中,一個(gè)在任務(wù)執(zhí)行期間發(fā)生的未處理異常通常會(huì)被推遲發(fā)現(xiàn)。例如,某個(gè)異常通常會(huì)被推遲到task_group::wait方法被調(diào)用時(shí)自動(dòng)發(fā)現(xiàn)。到那時(shí),該異常會(huì)在當(dāng)前所調(diào)用的wait方法中再次被拋出,這種方式讓我們得以在并行程序中使用串行式的異常處理方法。在PPL中,任務(wù)會(huì)推遲異常的上報(bào),直至調(diào)用任務(wù)組方法wait時(shí)才會(huì)再次拋出。3.1基本用法盡管每一個(gè)任務(wù)實(shí)際上都是一組串行化操作,但任務(wù)本身的執(zhí)行是實(shí)現(xiàn)并行化的。請(qǐng)看下面這兩行串行的代碼:DoLeft();DoRight();讓我們先來(lái)假設(shè)一下,如果這里的DoLeft和DoRight都是可以獨(dú)立執(zhí)行的函數(shù),即它們不會(huì)一個(gè)方法讀另一個(gè)方法寫(xiě)同樣的本地內(nèi)存區(qū)域或者磁盤(pán)文件。我們就可以將它們并行化。為了實(shí)現(xiàn)這一點(diǎn),我們需要調(diào)用Concurrency命名空間中的parallel_invoke函數(shù)。像下面這樣:parallel_invoke([](){DoLeft();},[](){DoRight();});Concurrency命名空間中的parallel_invoke函數(shù)能在系統(tǒng)中建立起一組并行任務(wù),并會(huì)等到這些任務(wù)完成之后再繼續(xù)。parallel_invoke函數(shù)是并行任務(wù)模式中最簡(jiǎn)單的實(shí)現(xiàn)形式了。該函數(shù)會(huì)將參數(shù)列表中的每一個(gè)lambda表達(dá)式都視作一個(gè)新的并行任務(wù),并為它們創(chuàng)建一個(gè)新的任務(wù)組來(lái)執(zhí)行。并在所有任務(wù)完成之后返回。正因?yàn)槿绱?,parallel_invoke函數(shù)參數(shù)列表中的那些函數(shù)也稱為工作函數(shù)(workfunction)。順帶一提,有些parallel_invoke函數(shù)的重載版本所支持的工作函數(shù)可以達(dá)到9個(gè)之多。需要注意的是,在我們使用并行任務(wù)時(shí),不能把設(shè)計(jì)思路建立在所有任務(wù)都會(huì)被立即執(zhí)行的基礎(chǔ)上。因?yàn)椋⑿腥蝿?wù)的運(yùn)行時(shí)機(jī)幾乎完全取決于它當(dāng)時(shí)所在的工作負(fù)載和系統(tǒng)配置,也就是說(shuō),任務(wù)調(diào)度器既有可能會(huì)依次調(diào)用它們,也有可能讓它們同時(shí)運(yùn)行。關(guān)于任務(wù)調(diào)度的細(xì)節(jié)問(wèn)題,后面再詳細(xì)介紹。在parallel_invoke中,任務(wù)函數(shù)除了以正常形式完成調(diào)用外,也有可能會(huì)因拋出異常而提前結(jié)束。如果在parallel_invoke函數(shù)運(yùn)行期間,某個(gè)工作函數(shù)拋出了異常,通常會(huì)推遲到所有任務(wù)執(zhí)行完成之后再上拋。如果拋出異常的工作函數(shù)不止一個(gè),運(yùn)行時(shí)環(huán)境會(huì)隨機(jī)選擇其中的一個(gè)來(lái)上拋,而其余的異常將不會(huì)被外部發(fā)現(xiàn)\h[3]。關(guān)于異常處理的詳細(xì)信息及其相關(guān)的代碼示例,我們將會(huì)在3.3.3節(jié)中進(jìn)一步說(shuō)明。從本質(zhì)上說(shuō),parallel_invoke函數(shù)的主要作用就是新建一系列任務(wù)并等待這些任務(wù)完成執(zhí)行。因而,我們完全可以自己建立一個(gè)任務(wù)組對(duì)象,并通過(guò)調(diào)用它的run和wait方法來(lái)實(shí)現(xiàn)同樣的功能。例如:task_grouptg;tg.run([](){DoLeft();});tg.run([](){DoRight();});tg.wait();任務(wù)組對(duì)象的run方法主要用于創(chuàng)建一個(gè)任務(wù)并完成它的調(diào)度與執(zhí)行;而wait方法則主要用于阻塞當(dāng)前環(huán)境,以待任務(wù)組執(zhí)行完所有的任務(wù)。上例通過(guò)task_group類的run方法新建了一個(gè)任務(wù),并完成了相關(guān)的執(zhí)行調(diào)度。實(shí)際上,run方法的參數(shù)是一個(gè)供任務(wù)執(zhí)行時(shí)所調(diào)用的函數(shù),它可以是一個(gè)lambda表達(dá)式、一個(gè)函數(shù)指針或者一個(gè)函數(shù)對(duì)象。換句話說(shuō),該參數(shù)可以是任何支持形如voidoperator()()函數(shù)調(diào)用運(yùn)算符的對(duì)象。task_group::run方法每新建一個(gè)任務(wù),就會(huì)將該任務(wù)加入到一個(gè)工作隊(duì)列中,以備最終執(zhí)行。但是只有在任務(wù)調(diào)度器將其調(diào)出隊(duì)列時(shí)才會(huì)執(zhí)行它,而這種出隊(duì)操作可以現(xiàn)在發(fā)生,也可以在將來(lái)的某個(gè)時(shí)間點(diǎn)上發(fā)生。如果傳遞給task_group::run方法的參數(shù)引用了某個(gè)支持operator()接口的類實(shí)例。那么,我們就必須有效地管理該函數(shù)對(duì)象所用的內(nèi)存,確保該對(duì)象在任務(wù)組的wait方法返回之后才被銷(xiāo)毀。而lambda表達(dá)式或者指向靜態(tài)函數(shù)的函數(shù)指針,并不需要顯式銷(xiāo)毀。從這點(diǎn)來(lái)說(shuō),這確實(shí)比f(wàn)unctors類類型(class-type)要方便得多。我們能通過(guò)調(diào)用任務(wù)組對(duì)象的wait方法來(lái)令其等待該任務(wù)組中的任務(wù)完成之后再返回?;蛟S有時(shí)候我們也可以將run和wait的操作步驟合二為一,
溫馨提示
- 1. 本站所有資源如無(wú)特殊說(shuō)明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁(yè)內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒(méi)有圖紙預(yù)覽就沒(méi)有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫(kù)網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。
最新文檔
- 2025年度體育賽事運(yùn)營(yíng)管理場(chǎng)規(guī)則與格式規(guī)范3篇
- 二零二四年度一致行動(dòng)人文化旅游產(chǎn)業(yè)合作協(xié)議合同3篇
- 2025年水電安裝工程設(shè)備采購(gòu)與安裝合同6篇
- 2025賓館與旅游公司聯(lián)合運(yùn)營(yíng)客房租賃合同范本2篇
- 2024物流企業(yè)稅收優(yōu)惠適用合同
- 2025年度充電樁充電樁項(xiàng)目融資與投資合同3篇
- 2025廠房買(mǎi)賣(mài)合同模板:工業(yè)地產(chǎn)投資合作框架3篇
- 2025年度龍門(mén)吊拆除設(shè)備再利用及資源化利用合同范本4篇
- 2025年度裝飾藝術(shù)玻璃定制銷(xiāo)售合同3篇
- 二零二四年倉(cāng)儲(chǔ)物流中心停車(chē)場(chǎng)租賃及倉(cāng)儲(chǔ)服務(wù)合同3篇
- 公司SWOT分析表模板
- 小學(xué)預(yù)防流行性感冒應(yīng)急預(yù)案
- 肺癌術(shù)后出血的觀察及護(hù)理
- 聲紋識(shí)別簡(jiǎn)介
- 生物醫(yī)藥大數(shù)據(jù)分析平臺(tái)建設(shè)-第1篇
- 基于Android的天氣預(yù)報(bào)系統(tǒng)的設(shè)計(jì)與實(shí)現(xiàn)
- 沖鋒舟駕駛培訓(xùn)課件
- 美術(shù)家協(xié)會(huì)會(huì)員申請(qǐng)表
- 聚合收款服務(wù)流程
- 中石化浙江石油分公司中石化溫州靈昆油庫(kù)及配套工程項(xiàng)目環(huán)境影響報(bào)告書(shū)
- 搞笑朗誦我愛(ài)上班臺(tái)詞
評(píng)論
0/150
提交評(píng)論