Java Streams,第 4 部分: 从并发到并行
2019獨(dú)角獸企業(yè)重金招聘Python工程師標(biāo)準(zhǔn)>>>
Java Streams,第 4 部分: 從并發(fā)到并行
Java Streams 系列的第 4 期文章將解釋決定并行處理的有效性的因素,從歷史和技術(shù)角度分析它們。了解這些因素是最高效地使用 Streams 庫(kù)實(shí)現(xiàn)并行執(zhí)行的基礎(chǔ),下一期文章 會(huì)重點(diǎn)介紹如何將這些原則直接應(yīng)用于 Streams。
更多核心,而不是更快的核心
從 2002 年左右開始,芯片設(shè)計(jì)者用于實(shí)現(xiàn)性能指數(shù)級(jí)增長(zhǎng)的技術(shù)開始枯竭。由于各種原因,進(jìn)一步提高時(shí)鐘頻率變得不切實(shí)際,包括功耗和散熱,而且在每個(gè)周期完成更多工作的技術(shù)( 指令級(jí)并行性 )所帶來(lái)的收益也已開始出現(xiàn)滑坡。
摩爾定律 預(yù)言,可集成到一個(gè)晶片上的晶體管數(shù)量約每?jī)赡攴环.?dāng)芯片設(shè)計(jì)者 2002 年遇到頻率瓶頸 時(shí),這并不是因?yàn)槟柖梢咽?#xff1b;我們可以看到,晶體管數(shù)量仍然穩(wěn)定地呈指數(shù)級(jí)增長(zhǎng)。不過,雖然利用這種不斷增加的晶體管預(yù)算來(lái)獲得更快核心的能力已失去作用,但芯片設(shè)計(jì)者仍能使用這種不斷增加的晶體管預(yù)算在單個(gè)晶片上放入更多核心。圖 1 通過來(lái)自英特爾處理器的數(shù)據(jù)演示了這種趨勢(shì),這些數(shù)據(jù)標(biāo)繪在一個(gè)對(duì)數(shù)標(biāo)尺上。最頂部的(直)線表示晶體管數(shù)量的指數(shù)級(jí)增長(zhǎng),表示時(shí)鐘頻率、功耗和指令級(jí)并行性的線在 2002 年左右都表現(xiàn)出明顯的趨平狀態(tài)。
圖 1. 英特爾 CPU 的晶體管數(shù)量和 CPU 性能(圖像來(lái)源:Herb Sutter)
擁有更多核心可實(shí)現(xiàn)更高的功率效率(未被主動(dòng)使用的核心可獨(dú)立地中斷電源),但這不一定等同于提供更高的程序性能 — 除非您可以保持所有核心都不停地執(zhí)行有用的工作。誠(chéng)然,如今的芯片并沒有為我們提供摩爾定律所允許的核心數(shù) — 主要是因?yàn)槿缃竦能浖o(wú)法富有成本效益地利用它們。
從并發(fā)到并行
幾乎在整個(gè)計(jì)算歷史中,并發(fā)性的目標(biāo)都是大致相同的 (通過增加 CPU 利用率來(lái)提高性能),但技術(shù)(和性能度量)卻已發(fā)生改變。在單核系統(tǒng)時(shí)代,并發(fā)性主要依靠的是異步性— 允許某個(gè)活動(dòng)在等待 I/O 完成期間讓出 CPU。異步性可提高響應(yīng)速度(在后臺(tái)活動(dòng)執(zhí)行期間不凍結(jié) UI)和吞吐量(在等待 I/O 完成期間允許另一個(gè)活動(dòng)使用 CPU)。在一些并發(fā)性模型(例如 Actors and Communicating Sequential Processes [CSP])中,并發(fā)性模型影響著程序結(jié)構(gòu),但在大多數(shù)情況下(不論好壞),我們僅根據(jù)性能需要來(lái)使用并發(fā)性。
影響并行性的有效性的因素包括問題本身、解決問題所使用的算法、對(duì)任務(wù)分解和調(diào)度的運(yùn)行時(shí)支持,以及數(shù)據(jù)集的大小和內(nèi)存位置。
隨著我們進(jìn)入多核時(shí)代,并發(fā)性的主要應(yīng)用是將工作負(fù)載分解為獨(dú)立的、粗粒度的任務(wù)(比如用戶請(qǐng)求),這樣做的目的在于通過同時(shí)處理多個(gè)請(qǐng)求來(lái)增加吞吐量。這一次,Java 庫(kù)獲得了一些工具,比如線程池 (thread pool)、信號(hào)量 (semaphore) 和 future,它們非常適合基于任務(wù)的并發(fā)性。
但是隨著核心數(shù)量的不斷增加,可能沒有足夠的 “自然發(fā)生的任務(wù)” 來(lái)讓所有核心一直處于繁忙狀態(tài)。隨著可用核心數(shù)超過任務(wù)數(shù),另一個(gè)性能目標(biāo)就變得很誘人:使用多個(gè)核心更快完成單個(gè)任務(wù)來(lái)減少延遲。不是所有任務(wù)都能通過這種分解來(lái)輕松處理;最適合的任務(wù)是數(shù)據(jù)密集型的查詢,其中的同一個(gè)操作會(huì)在大型的數(shù)據(jù)集上完成。
不幸的是,術(shù)語(yǔ) 并發(fā)性 和 并行性 沒有標(biāo)準(zhǔn)定義,它們常常被(錯(cuò)誤地)交替使用。在過去,并發(fā)性描述的是程序的一個(gè)屬性(程序結(jié)構(gòu)所實(shí)現(xiàn)的合作計(jì)算活動(dòng)的交互程度),而并行性是程序的一個(gè)執(zhí)行屬性,描述事件實(shí)際同時(shí)發(fā)生的程度。(根據(jù)此定義,并發(fā)性是并行性的潛在能力。)這種區(qū)別在真實(shí)的并發(fā)執(zhí)行主要停留在理論階段時(shí)很有用,但隨著時(shí)間的推移,有用性變得越來(lái)越低。
更多現(xiàn)代課程將并發(fā)性描述為正確、高效地控制對(duì)共享資源的訪問,而并行性指的是使用更多資源更快地解決問題。構(gòu)造線程安全的數(shù)據(jù)結(jié)構(gòu)屬于并發(fā)性范疇,通過鎖、事件、信號(hào)量、協(xié)同程序或軟件事務(wù)內(nèi)存 (STM) 等原語(yǔ)實(shí)現(xiàn)。另一方面,并行性使用分區(qū)或分片等技術(shù)來(lái)使多個(gè)活動(dòng)處理一項(xiàng)任務(wù),而沒有協(xié)調(diào)過程。
為什么這一區(qū)別很重要?畢竟,并發(fā)性和并行性的目標(biāo)都是同時(shí)完成多件事。但是,應(yīng)用這兩種技術(shù)的輕松程度有著很大差別。使用鎖等協(xié)調(diào)原語(yǔ)正確創(chuàng)建并發(fā)代碼很難、容易出錯(cuò)且不自然。通過讓每個(gè)工作者擁有自己的問題部分來(lái)處理問題,從而正確創(chuàng)建并行代碼,這樣做相對(duì)更簡(jiǎn)單、更安全且更自然。
并行性
盡管并行性的原理通常很簡(jiǎn)單,但難點(diǎn)在于知道何時(shí)使用它。嚴(yán)格地講,并行性是一種優(yōu)化;它是一種為一次特定計(jì)算使用更多資源的選擇,以期更快獲得答案,而您始終有權(quán)選擇按順序執(zhí)行計(jì)算。不幸的是,使用更多資源并不能保證更快(甚至快速)獲得答案。此外,如果并行性消耗了額外的資源卻沒有為我們帶來(lái)收益(或負(fù)面收益),我們不應(yīng)使用它。當(dāng)然,收益高低取決于環(huán)境,所以沒有統(tǒng)一的答案。但是我們擁有各種工具,可幫助評(píng)估在給定情形中能否有效使用并行性:分析、度量和性能需求。
本文主要介紹分析(和探索)哪些計(jì)算種類可以很好地并行化,哪些不能。但是,作為經(jīng)驗(yàn)規(guī)則,除非您有理由相信并行性會(huì)帶來(lái)提速,而且獲得的提速依據(jù)性能需求具有實(shí)際意義,否則會(huì)優(yōu)先使用順序執(zhí)行方法。(許多程序已足夠快,所以優(yōu)化它們所花的工作可更好地花在能創(chuàng)造更多價(jià)值的活動(dòng)上,比如提高適用性或可靠性。)
可以用提速來(lái)度量并行性有效性,提速是并行運(yùn)行時(shí)間與順序運(yùn)行時(shí)間的比率。選擇并行性(假設(shè)它能帶來(lái)提速)是對(duì)注重時(shí)間而不是 CPU 和功率利用率的謹(jǐn)慎選擇。并行執(zhí)行完成的工作始終比順序執(zhí)行的多,因?yàn)槌私鉀Q問題之外,它還必須分解問題,創(chuàng)建和管理描述子問題的任務(wù),分派和等待這些任務(wù),合并它們的結(jié)果。所以并行執(zhí)行始終在順序執(zhí)行 “之后” 開始,希望通過規(guī)模經(jīng)濟(jì)來(lái)補(bǔ)償最初的落后。
要讓并行性成為更好的選擇,必須綜合考慮多個(gè)方面。首先我們需要一個(gè)允許采用并行解決方案的問題 — 不是所有問題都允許采用并行解決方案。然后,我們需要實(shí)現(xiàn)利用了內(nèi)在并行性的解決方案。我們需要確保用來(lái)實(shí)現(xiàn)并行性的技術(shù)沒有太多開銷,以至于浪費(fèi)花在問題上的周期。而且我們還需要足夠的數(shù)據(jù),以便可以實(shí)現(xiàn)獲得提速所需的規(guī)模經(jīng)濟(jì)。
利用并行性
不是所有問題都適合并行化。考慮以下問題:給定一個(gè)函數(shù) f,我們假設(shè)計(jì)算成本很高,將 g 定義如下:
g(0) = f(0)
g(n) = f( g(n-1) ), for n > 0
我們可以為 g 實(shí)現(xiàn)一個(gè)并行算法并度量它的提速,但我們不需要這么做;我們可以查看問題,而且立即就會(huì)發(fā)現(xiàn)它完全是順序的。為了看到此結(jié)果,我們可以采用稍微不同的方式重寫 g(n):
g(n) = f( f( ... n times ... f(0) ... ) )
重寫之后,我們看到只有在 g(n-1) 計(jì)算完成后才能開始計(jì)算 g(n)。在計(jì)算 g(4) 的數(shù)據(jù)流依賴關(guān)系圖中,如圖 2 所示,g(n) 的每個(gè)值都依賴于前一個(gè)值 g(n-1)。
圖 2. 函數(shù) g 的數(shù)據(jù)流依賴關(guān)系圖
您可能忍不住得出這樣的判斷:問題源于 g(n) 是以遞歸方式定義的,但其實(shí)不然。考慮計(jì)算函數(shù) h(n) 的稍微不同的問題:
h(0) = f(0)
h(n) = f(n) + h(n-1)
如果編寫計(jì)算 h(n) 的比較明顯的實(shí)現(xiàn),我們最終會(huì)得到一個(gè)類似圖 3 的數(shù)據(jù)流依賴關(guān)系圖,其中每個(gè) h(n) 依賴于 h(n-1)。
圖 3. 函數(shù) h 的一種草率實(shí)現(xiàn)的數(shù)據(jù)流依賴關(guān)系圖
但是,如果以不同方式重寫 h(n),就可以立即看到此問題可以通過并行性解決。我們可以獨(dú)立地計(jì)算每一項(xiàng),然后對(duì)它們求和(這也允許并行性):
h(n) = f(0) + f(1) + .. + f(n)
結(jié)果會(huì)得到圖 4 中所示的數(shù)據(jù)流依賴關(guān)系圖,其中每個(gè) h(n) 均可獨(dú)立計(jì)算。
圖 4. 函數(shù) h 的數(shù)據(jù)流依賴關(guān)系圖
這些示例表明了兩點(diǎn):首先,看起來(lái)類似的問題可能擁有完全不同的并行性可利用程度;第二,一個(gè)可利用并行性解決的問題的解決方案的 “明顯” 實(shí)現(xiàn)不一定會(huì)利用并行性。要獲得提速機(jī)會(huì),需要同時(shí)滿足兩個(gè)條件。
分而治之
實(shí)現(xiàn)可利用的并行性的標(biāo)準(zhǔn)技術(shù)稱為遞歸分解 或分而治之。在此方法中,會(huì)反復(fù)將問題分解為許多子問題,直到子問題小到比按照順序解決更有意義;我們會(huì)并行解決子問題,然后組合子問題的各部分結(jié)果來(lái)獲得總結(jié)果。
清單 1 使用偽代碼演示了一個(gè)典型的分而治之解決方案,為并發(fā)執(zhí)行使用了一個(gè)假想的 CONCURRENT 原語(yǔ)。
清單 1.遞歸分解
R solve(Problem<R> problem) {if (problem.isSmall())return problem.solveSequentially();R leftResult, rightResult;CONCURRENT {leftResult = solve(problem.leftHalf());rightResult = solve(problem.rightHalf());}return problem.combine(leftResult, rightResult); }遞歸分解很吸引人,因?yàn)樗芎?jiǎn)單(在處理已遞歸定義的數(shù)據(jù)結(jié)構(gòu)(比如樹)時(shí)尤其如此)。類似清單 1 的并行代碼可跨眾多計(jì)算環(huán)境進(jìn)行移植;它能夠使用一個(gè)核心或許多核心高效地處理數(shù)據(jù),而且它不需要擔(dān)心協(xié)調(diào)對(duì)共享可變狀態(tài)的訪問帶來(lái)復(fù)雜性 — 因?yàn)闆]有共享可變狀態(tài)。將問題分解為若干個(gè)子問題,安排每個(gè)問題僅訪問來(lái)自某個(gè)特定子問題的數(shù)據(jù),這通常不需要進(jìn)行線程間協(xié)調(diào)。
性能考慮因素
使用 清單 1 作為模型,我們現(xiàn)在可以繼續(xù)分析并行性可帶來(lái)優(yōu)勢(shì)的條件。通過分而治之方法引入了兩個(gè)額外的算法步驟(分解問題和組合部分結(jié)果),每個(gè)步驟或多或少適合并行性。另一個(gè)可能影響并行性能的因素是并行性原語(yǔ)本身的效率,我們對(duì) 清單 1 的偽代碼中假想的 CONCURRENT 機(jī)制進(jìn)行了演示。其他兩個(gè)因素是數(shù)據(jù)集的屬性:數(shù)據(jù)集的大小和它的內(nèi)存位置。我們將依次查看每個(gè)條件。
一些問題(比如 “利用并行性” 部分的 g(n) 函數(shù))完全不允許使用分解。甚至在問題允許采用分解時(shí),分解可能也需要成本。(至少,分解一個(gè)問題涉及到創(chuàng)建子問題的描述。)例如,Quicksort 算法的分解步驟需要找到一個(gè)支點(diǎn),也就是問題大小中的 O(n),因?yàn)樗婕暗綑z查并(可能)更新所有數(shù)據(jù)。此需求比 “對(duì)一個(gè)元素?cái)?shù)組求和” 這樣的問題的分解步驟的需求要高得多,后者的分解步驟為 O(1)— “對(duì)最高和最低指數(shù)求平均值。”此外,即使可以高效地分解問題,如果兩個(gè)子問題的大小非常不均勻,我們可能不會(huì)獲得太多可利用的并行性。
類似地,在解決兩個(gè)子問題時(shí),必須組合結(jié)果。如果我們的問題是 “刪除重復(fù)元素”,步驟的組合需要合并兩個(gè)集合;如果我們想要獲得結(jié)果的一種平面表示,此步驟也為 O(n)。另一方面,如果我們的問題是 “對(duì)一個(gè)數(shù)組求和”,我們的組合步驟也是 O(1)— “添加兩個(gè)數(shù)”。
管理要并發(fā)執(zhí)行的任務(wù)可能涉及到多個(gè)可能的效率損失來(lái)源,比如將數(shù)據(jù)從一個(gè)線程轉(zhuǎn)交給另一個(gè)線程的內(nèi)在延遲,或者爭(zhēng)用共享數(shù)據(jù)結(jié)構(gòu)的風(fēng)險(xiǎn)。fork-join 框架(Java SE 7 中添加來(lái)管理細(xì)粒度、計(jì)算密集型任務(wù))旨在最大限度減少并行分派中許多常見的低效性來(lái)源。java.util.stream 庫(kù)使用 fork-join 框架實(shí)現(xiàn)并行執(zhí)行。
最后,我們必須考慮數(shù)據(jù)。如果數(shù)據(jù)集很小,則很難獲得任何提速,原因是并行執(zhí)行會(huì)帶來(lái)啟動(dòng)成本。類似地,如果數(shù)據(jù)集所在的內(nèi)存位置不佳(指針眾多的數(shù)據(jù)結(jié)構(gòu),比如圖表,而不是數(shù)組),利用如今典型的內(nèi)存受限的系統(tǒng)執(zhí)行并行可能會(huì)導(dǎo)致許多線程等待來(lái)自緩存的數(shù)據(jù),而不是有效使用核心來(lái)更快地計(jì)算答案。
這些因素中的每一個(gè)都有可能降低提速;在某些情況下,它們不僅會(huì)降低提速,甚至還會(huì)導(dǎo)致減速。
阿姆達(dá)爾定律
阿姆達(dá)爾定律描述計(jì)算的順序部分對(duì)可能的并行提速有何限制。大部分問題都有一定數(shù)量的工作無(wú)法并行化;這部分工作被稱為串行分?jǐn)?shù) (serial fraction)。例如,如果將從一個(gè)數(shù)組將數(shù)據(jù)復(fù)制到另一個(gè)數(shù)組,復(fù)制過程可以并行化,但目標(biāo)數(shù)組的分配(具有內(nèi)在的順序性)必須在發(fā)生任何復(fù)制之前執(zhí)行。
圖 5 展示了阿姆達(dá)爾定律的效果。各種曲線演示了此定律所允許的可能的最佳加速比,是如何隨著可獲得的處理器數(shù)量的變化的同時(shí),對(duì)于不同的并行分?jǐn)?shù)(串行分?jǐn)?shù)的補(bǔ)集),阿姆達(dá)爾定律所允許的最大提速。舉例而言,如果并行碎片為 .5(一半的問題必須順序執(zhí)行)且有無(wú)限多個(gè)處理器可用,阿姆達(dá)爾定律會(huì)告訴我們,我們有望實(shí)現(xiàn)的最大提速為 2 倍。結(jié)果一目了然;即使我們可以通過并行化將可并行化部分的成本減少到 0,我們?nèi)杂幸话氲膯栴}要順序解決。
圖 5. 阿姆達(dá)爾定律(圖像來(lái)源:Wikimedia Commons)
阿姆達(dá)爾定律暗示的模型(工作的一些碎片必須完全順序地執(zhí)行,剩余部分可完美地并行化)過于簡(jiǎn)單。不過,該模型仍對(duì)理解阻礙并行性的因素很有用。查明和減少串行碎片的能力,是能夠設(shè)計(jì)出更高效的并行算法的關(guān)鍵因素。
阿姆達(dá)爾定律的另一種解釋是:如果您有一臺(tái) 32 核的機(jī)器,對(duì)于您設(shè)置并行計(jì)算所花的每個(gè)周期,有 31 個(gè)周期不能應(yīng)用來(lái)解決問題。而且如果您將問題拆分為兩個(gè)子問題,每個(gè)時(shí)鐘周期仍將浪費(fèi) 30 個(gè)周期。只有當(dāng)您拆分足夠的工作來(lái)讓所有處理器保持繁忙時(shí),您才會(huì)獲得全速性能 — 而且如果您沒有足夠快地達(dá)到全速性能(或在此狀態(tài)停留足夠長(zhǎng)時(shí)間),將很難獲得不錯(cuò)的提速。
結(jié)束語(yǔ)
并行性是一種權(quán)衡,它使用更多計(jì)算資源來(lái)希望獲得提速。盡管從理論上講,我們使用 N 個(gè)核心可將問題解決速度提高 N 倍,但現(xiàn)實(shí)通常離此目標(biāo)相距甚遠(yuǎn)。影響并行性有效性的因素包括問題本身、解決問題所使用的算法、對(duì)任務(wù)分解和調(diào)度的運(yùn)行時(shí)支持,以及數(shù)據(jù)集的大小和內(nèi)存位置。Java Streams 的 第 5 部分 會(huì)將這些考慮因素應(yīng)用到 Streams 庫(kù),并展示一些流管道如何(和為什么能)比其他管道更有效地實(shí)現(xiàn)并行化。
轉(zhuǎn)載于:https://my.oschina.net/CasparLi/blog/830417
總結(jié)
以上是生活随笔為你收集整理的Java Streams,第 4 部分: 从并发到并行的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: VS2010+WinXP+MFC程序 无
- 下一篇: HTML+CSS的学习