主要原因有兩個(gè):關(guān)注點(diǎn)分離(SOC)和性能。事實(shí)上,它們應(yīng)該是使用并發(fā)的唯一原因;如果你觀察得足夠仔細(xì),所有因素都可以歸結(jié)到其中的一個(gè)原因(或者可能是兩個(gè)都有。當(dāng)然,除了像“就因?yàn)槲以敢狻边@樣的原因之外)。
編寫軟件時(shí),分離關(guān)注點(diǎn)是個(gè)好主意;通過(guò)將相關(guān)的代碼與無(wú)關(guān)的代碼分離,可以使程序更容易理解和測(cè)試,從而減少出錯(cuò)的可能性。即使一些功能區(qū)域中的操作需要在同一時(shí)刻發(fā)生的情況下,依舊可以使用并發(fā)分離不同的功能區(qū)域;若不顯式地使用并發(fā),就得編寫一個(gè)任務(wù)切換框架,或者在操作中主動(dòng)地調(diào)用一段不相關(guān)的代碼。
考慮一個(gè)有用戶界面的處理密集型應(yīng)用——DVD播放程序。這樣的應(yīng)用程序,應(yīng)具備這兩種功能:一,要從光盤中讀出數(shù)據(jù),對(duì)圖像和聲音進(jìn)行解碼,之后把解碼出的信號(hào)輸出至視頻和音頻硬件,從而實(shí)現(xiàn)DVD的無(wú)誤播放;二,還需要接受來(lái)自用戶的輸入,當(dāng)用戶單擊“暫?!?、“返回菜單”或“退出”按鍵的時(shí)候執(zhí)行對(duì)應(yīng)的操作。當(dāng)應(yīng)用是單個(gè)線程時(shí),應(yīng)用需要在回放期間定期檢查用戶的輸入,這就需要把“DVD播放”代碼和“用戶界面”代碼放在一起,以便調(diào)用。如果使用多線程方式來(lái)分隔這些關(guān)注點(diǎn),“用戶界面”代碼和“DVD播放”代碼就不再需要放在一起:一個(gè)線程可以處理“用戶界面”事件,另一個(gè)進(jìn)行“DVD播放”。它們之間會(huì)有交互(用戶點(diǎn)擊“暫?!?,不過(guò)任務(wù)間需要人為的進(jìn)行關(guān)聯(lián)。
這會(huì)給響應(yīng)性帶來(lái)一些錯(cuò)覺(jué),因?yàn)橛脩艚缑婢€程通常可以立即響應(yīng)用戶的請(qǐng)求,在當(dāng)請(qǐng)求傳達(dá)給忙碌線程,這時(shí)的相應(yīng)可以是簡(jiǎn)單地顯示代表忙碌的光標(biāo)或“請(qǐng)等待”字樣的消息。類似地,獨(dú)立的線程通常用來(lái)執(zhí)行那些必須在后臺(tái)持續(xù)運(yùn)行的任務(wù),例如,桌面搜索程序中監(jiān)視文件系統(tǒng)變化的任務(wù)。因?yàn)樗鼈冎g的交互清晰可辨,所以這種方式會(huì)使每個(gè)線程的邏輯變的更加簡(jiǎn)單。
在這種情況下,線程的數(shù)量不再依賴CPU中的可用內(nèi)核的數(shù)量,因?yàn)閷?duì)線程的劃分是基于概念上的設(shè)計(jì),而不是一種增加吞吐量的嘗試。
多處理器系統(tǒng)已經(jīng)存在了幾十年,但直到最近,它們也只在超級(jí)計(jì)算機(jī)、大型機(jī)和大型服務(wù)器系統(tǒng)中才能看到。然而,芯片制造商越來(lái)越傾向于多核芯片的設(shè)計(jì),即在單個(gè)芯片上集成2、4、16或更多的處理器,從而獲取更好的性能。因此,多核臺(tái)式計(jì)算機(jī)、多核嵌入式設(shè)備,現(xiàn)在越來(lái)越普遍。它們計(jì)算能力的提高不是源自使單一任務(wù)運(yùn)行的更快,而是并行運(yùn)行多個(gè)任務(wù)。在過(guò)去,程序員曾坐看他們的程序隨著處理器的更新?lián)Q代而變得更快,無(wú)需他們這邊做任何事。但是現(xiàn)在,就像Herb Sutter所說(shuō)的,“沒(méi)有免費(fèi)的午餐了?!盵1] 如果想要利用日益增長(zhǎng)的計(jì)算能力,那就必須設(shè)計(jì)多任務(wù)并發(fā)式軟件。程序員必須留意這個(gè),尤其是那些迄今都忽略并發(fā)的人們,現(xiàn)在很有必要將其加入工具箱中了。
兩種方式利用并發(fā)提高性能:第一,將一個(gè)單個(gè)任務(wù)分成幾部分,且各自并行運(yùn)行,從而降低總運(yùn)行時(shí)間。這就是任務(wù)并行(task parallelism)。雖然這聽(tīng)起來(lái)很直觀,但它是一個(gè)相當(dāng)復(fù)雜的過(guò)程,因?yàn)樵诟鱾€(gè)部分之間可能存在著依賴。區(qū)別可能是在過(guò)程方面——一個(gè)線程執(zhí)行算法的一部分,而另一個(gè)線程執(zhí)行算法的另一個(gè)部分——或是在數(shù)據(jù)方面——每個(gè)線程在不同的數(shù)據(jù)部分上執(zhí)行相同的操作(第二種方式)。后一種方法被稱為數(shù)據(jù)并行(data parallelism)。
第一種并行方式影響的算法常被稱為易并行(embarrassingly parallel)算法。盡管易并行算法的代碼會(huì)讓你感覺(jué)到頭痛,但這對(duì)于你來(lái)說(shuō)是一件好事:我曾遇到過(guò)自然并行(naturally parallel)和便利并發(fā)(conveniently concurrent)的算法。易并行算法具有良好的可擴(kuò)展特性——當(dāng)可用硬件線程的數(shù)量增加時(shí),算法的并行性也會(huì)隨之增加。這種算法能很好的體現(xiàn)人多力量大。如果算法中有不易并行的部分,你可以把算法劃分成固定(不可擴(kuò)展)數(shù)量的并行任務(wù)。第8章將會(huì)再來(lái)討論,在線程之間劃分任務(wù)的技巧。
第二種方法是使用可并行的方式,來(lái)解決更大的問(wèn)題;與其同時(shí)處理一個(gè)文件,不如酌情處理2個(gè)、10個(gè)或20個(gè)。雖然,這是數(shù)據(jù)并行的一種應(yīng)用(通過(guò)對(duì)多組數(shù)據(jù)同時(shí)執(zhí)行相同的操作),但著重點(diǎn)不同。處理一個(gè)數(shù)據(jù)塊仍然需要同樣的時(shí)間,但在相同的時(shí)間內(nèi)處理了更多的數(shù)據(jù)。當(dāng)然,這種方法也有限制,并非在所有情況下都是有益的。不過(guò),這種方法所帶來(lái)的吞吐量提升,可以讓某些新功能成為可能,例如,可以并行處理圖片的各部分,就能提高視頻的分辨率。
知道何時(shí)不使用并發(fā)與知道何時(shí)使用它一樣重要?;旧希皇褂貌l(fā)的唯一原因就是,收益比不上成本。使用并發(fā)的代碼在很多情況下難以理解,因此編寫和維護(hù)的多線程代碼就會(huì)產(chǎn)生直接的腦力成本,同時(shí)額外的復(fù)雜性也可能引起更多的錯(cuò)誤。除非潛在的性能增益足夠大或關(guān)注點(diǎn)分離地足夠清晰,能抵消所需的額外的開(kāi)發(fā)時(shí)間以及與維護(hù)多線程代碼相關(guān)的額外成本(代碼正確的前提下);否則,別用并發(fā)。
同樣地,性能增益可能會(huì)小于預(yù)期;因?yàn)椴僮飨到y(tǒng)需要分配內(nèi)核相關(guān)資源和堆??臻g,所以在啟動(dòng)線程時(shí)存在固有的開(kāi)銷,然后才能把新線程加入調(diào)度器中,所有這一切都需要時(shí)間。如果在線程上的任務(wù)完成得很快,那么任務(wù)實(shí)際執(zhí)行的時(shí)間要比啟動(dòng)線程的時(shí)間小很多,這就會(huì)導(dǎo)致應(yīng)用程序的整體性能還不如直接使用“產(chǎn)生線程”的方式。
此外,線程是有限的資源。如果讓太多的線程同時(shí)運(yùn)行,則會(huì)消耗很多操作系統(tǒng)資源,從而使得操作系統(tǒng)整體上運(yùn)行得更加緩慢。不僅如此,因?yàn)槊總€(gè)線程都需要一個(gè)獨(dú)立的堆??臻g,所以運(yùn)行太多的線程也會(huì)耗盡進(jìn)程的可用內(nèi)存或地址空間。對(duì)于一個(gè)可用地址空間為4GB(32bit)的平坦架構(gòu)的進(jìn)程來(lái)說(shuō),這的確是個(gè)問(wèn)題:如果每個(gè)線程都有一個(gè)1MB的堆棧(很多系統(tǒng)都會(huì)這樣分配),那么4096個(gè)線程將會(huì)用盡所有地址空間,不會(huì)給代碼、靜態(tài)數(shù)據(jù)或者堆數(shù)據(jù)留有任何空間。即便64位(或者更大)的系統(tǒng)不存在這種直接的地址空間限制,但其他資源有限:如果你運(yùn)行了太多的線程,最終也是出會(huì)問(wèn)題的。盡管線程池(參見(jiàn)第9章)可以用來(lái)限制線程的數(shù)量,但這也并不是什么靈丹妙藥,它也有自己的問(wèn)題。
當(dāng)客戶端/服務(wù)器(C/S)應(yīng)用在服務(wù)器端為每一個(gè)鏈接啟動(dòng)一個(gè)獨(dú)立的線程,對(duì)于少量的鏈接是可以正常工作的,但當(dāng)同樣的技術(shù)用于需要處理大量鏈接的高需求服務(wù)器時(shí),也會(huì)因?yàn)榫€程太多而耗盡系統(tǒng)資源。在這種場(chǎng)景下,使用線程池可以對(duì)性能產(chǎn)生優(yōu)化(參見(jiàn)第9章)。
最后,運(yùn)行越多的線程,操作系統(tǒng)就需要做越多的上下文切換,每一次切換都需要耗費(fèi)本可以花在有價(jià)值工作上的時(shí)間。所以在某些時(shí)候,增加一個(gè)額外的線程實(shí)際上會(huì)降低,而非提高應(yīng)用程序的整體性能。為此,如果你試圖得到系統(tǒng)的最佳性能,可以考慮使用硬件并發(fā)(或不用),并調(diào)整運(yùn)行線程的數(shù)量。
為性能而使用并發(fā)就像所有其他優(yōu)化策略一樣:它擁有大幅度提高應(yīng)用性能的潛力,但它也可能使代碼復(fù)雜化,使其更難理解,并更容易出錯(cuò)。因此,只有應(yīng)用中具有顯著增益潛力的性能關(guān)鍵部分,才值得并發(fā)化。當(dāng)然,如果性能收益的潛力僅次于設(shè)計(jì)清晰或關(guān)注點(diǎn)分離,可能也值得使用多線程設(shè)計(jì)。
假設(shè)你已經(jīng)決定確實(shí)要在應(yīng)用中使用并發(fā),無(wú)論是為了性能、關(guān)注點(diǎn)分離,亦或是因?yàn)?em>多線程星期一(multithreading Monday)(譯者:可能是學(xué)習(xí)多線程的意思)。
問(wèn)題又來(lái)了,對(duì)于C++程序員來(lái)說(shuō),多線程意味著什么?
[1] “The Free Lunch Is Over: A Fundamental Turn Toward Concurrency in Software,” Herb Sutter, Dr. Dobb’s Journal, 30(3), March 2005. http://www.gotw.ca/publications/concurrency-ddj.htm.