有什么好的并发编程书籍推荐?还真有一本
今天小編要說的是《C++并發(fā)編程實戰(zhàn)》(第2版)這本書,很多程序員都知道這本書。第2版全新翻譯,給你一個不一樣的閱讀體驗。
《C++并發(fā)編程實戰(zhàn)》(第2版)由C++標(biāo)準(zhǔn)委員會成員編寫,囊括C++并發(fā)編程多個方面。作者Anthony Williams獨自起草并參與編寫了許多與多線程和并發(fā)相關(guān)的提案,這些提案塑造了C++標(biāo)準(zhǔn)的一部分。Anthony Williams持續(xù)參與了C++標(biāo)準(zhǔn)委員會并發(fā)小組的工作,包括對C++17標(biāo)準(zhǔn)進行改進,制定并發(fā)技術(shù)規(guī)約(Concurrency Technical Specification),以及編寫關(guān)于C++未來演化發(fā)展的提案等。
- 這是一本介紹C++并發(fā)和多線編程的深度指南,囊括了C++并發(fā)編程的多個方面,涉及啟動新線程以及設(shè)計全功能的多線程算法和數(shù)據(jù)結(jié)構(gòu)等核心知識點;
- 本書譯文經(jīng)過反復(fù)推敲,作譯者協(xié)同參與全書內(nèi)容的翻譯和審讀,代碼配有詳細(xì)的中文注釋,內(nèi)容簡潔易懂;
- 譯者還基于自己的開發(fā)經(jīng)驗,補充了許多延伸知識點,適合想要深入了解C++多線程的開發(fā)人員深入學(xué)習(xí);
- 本書提供強大的配套資源,包括近200頁的電子版附錄D以及140多份配套代碼文件。
本書內(nèi)容是 C++新標(biāo)準(zhǔn)中涉及的并發(fā)與多線程功能,從std::thread、std::mutex和std::async的基本使用方法開始,一直到復(fù)雜的內(nèi)存模型和原子操作。
假若讀者之前沒使用過C++11的新功能,那就需要先瀏覽一下附錄A,再開始閱讀正文,這將有助于透徹理解本書的代碼示例。正文中已經(jīng)標(biāo)注出用到C++新特性的地方,盡管如此,一旦你遇到任何從未見過的內(nèi)容,也可以隨時翻查附錄。
如果讀者已經(jīng)編寫過多線程代碼,并且經(jīng)驗豐富,前幾章會讓你知曉已經(jīng)熟知的工具與新標(biāo)準(zhǔn)的C++工具是怎樣對應(yīng)的。倘若讀者要進行任何底層工作,涉及原子變量,則第5章必不可少。為了確保讀者真正熟知C++多線程編程的各種細(xì)節(jié),例如異常安全(exception safety),那么,第8章值得好好學(xué)習(xí)。如果讀者肩負(fù)某種特定的編碼任務(wù),索引和目錄會幫你迅速定位到有關(guān)章節(jié)。
即便你已經(jīng)掌握了C++線程庫的使用方法,附錄D(可從異步社區(qū)下載)依然有用,例如可供你查閱各個類和函數(shù)調(diào)用的精準(zhǔn)細(xì)節(jié)。你也可以考慮時不時地回顧一下主要章節(jié),或強化記憶某個特定的模型,或重溫示例代碼。
簡要目錄
- 第1章 你好,C++并發(fā)世界
- 第2章 線程管控
- 第3章 在線程間共享數(shù)據(jù)
- 第4章 并發(fā)操作的同步
- 第5章 C++內(nèi)存模型和原子操作
- 第6章 設(shè)計基于鎖的并發(fā)數(shù)據(jù)結(jié)構(gòu)
- 第7章 設(shè)計無鎖數(shù)據(jù)結(jié)構(gòu)
- 第8章 設(shè)計并發(fā)代碼
- 第9章 高級線程管理
- 第10章 并行算法函數(shù)
- 第11章 多線程應(yīng)用的測試和除錯
樣章試讀:第1章 你好,C++并發(fā)世界
1.1 什么是并發(fā)
按最簡單、最基本的程度理解,并發(fā)(concurrency)是兩個或多個同時獨立進行的活動。并發(fā)現(xiàn)象遍布日常生活,我們時常接觸:我們可以邊走路邊說話;或者,左右手同時做出不一樣的動作;我們每個人也都可以獨立行事——當(dāng)我游泳時,你可以觀看足球比賽;諸如此類。
1.1.1 計算機系統(tǒng)中的并發(fā)
若我們談及計算機系統(tǒng)中的并發(fā),則是指同一個系統(tǒng)中,多個獨立活動同時進行,而非依次進行。這不足為奇。多年來,多任務(wù)操作系統(tǒng)可以憑借任務(wù)切換,讓同一臺計算機同時運行多個應(yīng)用軟件,這早已稀松平常,而高端服務(wù)器配備了多處理器,實現(xiàn)了“真并發(fā)”(genuine concurrency)。大勢所趨,主流計算機現(xiàn)已能夠真真正正地并行處理多任務(wù),而不再只是制造并發(fā)的表象。
很久之前,大多計算機都僅有一個處理器,處理器內(nèi)只有單一處理單元或單個內(nèi)核,許多臺式計算機至今依舊如此。這種計算機在同一時刻實質(zhì)上只能處理一個任務(wù),不過,每秒內(nèi),它可以在各個任務(wù)之間多次切換,先處理某任務(wù)的一小部分,接著切換任務(wù),同樣只處理一小部分,然后對其他任務(wù)如法炮制。于是,看起來所有任務(wù)都正在同時執(zhí)行。因此其被稱為任務(wù)切換。至此,我們談及的并發(fā)都基于這種模式。由于任務(wù)飛速切換,我們難以分辨處理器到底在哪一刻暫停某個任務(wù)而切換到另一個。任務(wù)切換對使用者和應(yīng)用軟件自身都制造出并發(fā)的表象。由于是表象,因此對比真正的并發(fā)環(huán)境,當(dāng)應(yīng)用程序在進行任務(wù)切換的單一處理器環(huán)境下運行時,其行為可能稍微不同。具體而言,如果就內(nèi)存模型(見第5章)做出不當(dāng)假設(shè),本來會導(dǎo)致某些問題,但這些問題在上述環(huán)境中卻有可能不會出現(xiàn)。第10章將對此深入討論。
多年來,配備了多處理器的計算機一直被用作服務(wù)器,它要承擔(dān)高性能的計算任務(wù);現(xiàn)今,基于一芯多核處理器(簡稱多核處理器)的計算機日漸普及,多核處理器也用在臺式計算機上。無論是裝配多個處理器,還是單個多核處理器,或是多個多核處理器,這些計算機都能真正并行運作多個任務(wù),我們稱之為硬件并發(fā)(hardware concurrency)。
圖1.1所示為理想化的情景。計算機有兩個任務(wù)要處理,將它們進行十等分。在雙核機(具有兩個處理核)上,兩個任務(wù)在各自的核上分別執(zhí)行。另一臺單核機則切換任務(wù),交替執(zhí)行任務(wù)小段,但任務(wù)小段之間略有間隔。在圖1.1中,單核機的任務(wù)小段被灰色小條隔開,它們比雙核機的分隔條粗大。為了交替執(zhí)行,每當(dāng)系統(tǒng)從某一個任務(wù)切換到另一個時,就必須完成一次上下文切換(context switch),于是耗費了時間。若要完成一次上下文切換,則操作系統(tǒng)需保存當(dāng)前任務(wù)的CPU狀態(tài)和指令指針[2],判定需要切換到哪個任務(wù),并為之重新加載CPU狀態(tài)。接著,CPU有可能需要將新任務(wù)的指令和數(shù)據(jù)從內(nèi)存加載到緩存,這或許會妨礙CPU,令其無法執(zhí)行任何指令,加劇延遲。
圖1.1 兩種并發(fā)方式:雙核機上的并發(fā)執(zhí)行與單核機上的任務(wù)切換
盡管多處理器或多核系統(tǒng)明顯更適合硬件并發(fā),不過有些處理器也能在單核上執(zhí)行多線程。真正需要注意的關(guān)鍵因素是硬件支持的線程數(shù)(hardware threads),也就是硬件自身真正支持同時運行的獨立任務(wù)的數(shù)量。即便是真正支持硬件并發(fā)的系統(tǒng),任務(wù)的數(shù)量往往容易超過硬件本身可以并行處理的數(shù)量,因而在這種情形下任務(wù)切換依然有用。譬如,常見的臺式計算機能夠同時運行數(shù)百個任務(wù),在后臺進行各種操作,表面上卻處于空閑狀態(tài)。正是由于任務(wù)切換,后臺任務(wù)才得以運作,才容許我們運行許多應(yīng)用軟件,如文字處理軟件、編譯器、編輯軟件,以及瀏覽器等。圖1.2展示了雙核機上4個任務(wù)的相互切換,這同樣是理想化的情形,各個任務(wù)都被均勻切分。實踐中,許多問題會導(dǎo)致任務(wù)切分不均勻或調(diào)度不規(guī)則。我們將在第8章探究影響并發(fā)代碼性能的因素,將解決上述某些問題。
圖1.2 4個任務(wù)在雙核機上切換
本書涉及的技術(shù)、函數(shù)和類適用于各種環(huán)境:無論負(fù)責(zé)運行的計算機是配備了單核單處理器,還是多核多處理器;無論其并發(fā)功能如何實現(xiàn),是憑借任務(wù)切換,還是真正的硬件并發(fā),一概不影響使用。然而,也許讀者會想到,應(yīng)用軟件如何充分利用并發(fā)功能,很大程度上取決于硬件所支持的并發(fā)任務(wù)數(shù)量。我們將在第8章講述設(shè)計并發(fā)的C++代碼的相關(guān)議題,也會涉及這點。
1.1.2 并發(fā)的方式
設(shè)想兩位開發(fā)者要共同開發(fā)一個軟件項目。假設(shè)他們處于兩間獨立的辦公室,而且各有一份參考手冊,則他們可以靜心工作,不會彼此干擾。但這令交流頗費周章:他們無法一轉(zhuǎn)身就與對方交談,遂不得不借助電話或郵件,或是需起身離座走到對方辦公室。另外,使用兩間辦公室有額外開支,還需購買多份參考手冊。
現(xiàn)在,如果安排兩位開發(fā)者共處一室,他們就能暢談軟件項目的設(shè)計,也便于在紙上或壁板上作圖,從而有助于交流設(shè)計的創(chuàng)意和理念。這樣,僅有一間辦公室要管理,并且各種資源通常只需一份就足夠了。但缺點是,他們恐怕難以集中精神,共享資源也可能出現(xiàn)問題。
這兩種安排開發(fā)者的辦法示意了并發(fā)的兩種基本方式。一位開發(fā)者代表一個線程,一間辦公室代表一個進程。第一種方式采用多個進程,各進程都只含單一線程,情況類似于每位開發(fā)者都有自己的辦公室;第二種方式只運行單一進程,內(nèi)含多個線程,正如兩位開發(fā)者同處一間辦公室。我們可以隨意組合這兩種方式,掌控多個進程,其中有些進程包含多線程,有些進程只包含單一線程,但基本原理相同。接著,我們來簡略看看應(yīng)用軟件中的這兩種并發(fā)方式。
1.多進程并發(fā)
在應(yīng)用軟件內(nèi)部,一種并發(fā)方式是,將一個應(yīng)用軟件拆分成多個獨立進程同時運行,它們都只含單一線程,非常類似于同時運行瀏覽器和文字處理軟件。這些獨立進程可以通過所有常規(guī)的進程間通信途徑相互傳遞信息(信號、套接字、文件、管道等),如圖1.3所示。這種進程間通信普遍存在短處:或設(shè)置復(fù)雜,或速度慢,甚至二者兼有,因為操作系統(tǒng)往往要在進程之間提供大量防護措施,以免某進程意外改動另一個進程的數(shù)據(jù);還有一個短處是運行多個進程的固定開銷大,進程的啟動花費時間,操作系統(tǒng)必須調(diào)配內(nèi)部資源來管控進程,等等。
圖1.3 兩個進程并發(fā)運行并相互通信
進程間通信并非一無是處:通常,操作系統(tǒng)在進程間提供額外保護和高級通信機制。這就意味著,比起線程,采用進程更容易編寫出安全的并發(fā)代碼。某些編程環(huán)境以進程作為基本構(gòu)建單元,其并發(fā)效果確實一流,譬如為Erlang編程語言準(zhǔn)備的環(huán)境。
運用獨立的進程實現(xiàn)并發(fā),還有一個額外優(yōu)勢——通過網(wǎng)絡(luò)連接,獨立的進程能夠在不同的計算機上運行。這樣做雖然增加了通信開銷,可是只要系統(tǒng)設(shè)計精良,此法足以低廉而有效地增強并發(fā)力度,改進性能。
2.多線程并發(fā)
另一種并發(fā)方式是在單一進程內(nèi)運行多線程。線程非常像輕量級進程:每個線程都獨立運行,并能各自執(zhí)行不同的指令序列。不過,同一進程內(nèi)的所有線程都共用相同的地址空間,且所有線程都能直接訪問大部分?jǐn)?shù)據(jù)。全局變量依然全局可見,指向?qū)ο蠡驍?shù)據(jù)的指針和引用能在線程間傳遞。盡管進程間共享內(nèi)存通常可行,但這種做法設(shè)置復(fù)雜,往往難以駕馭,原因是同一數(shù)據(jù)的地址在不同進程中不一定相同。圖1.4展示了單一進程內(nèi)的兩個線程借共享內(nèi)存通信。
圖1.4 單一進程內(nèi)的兩個線程借共享內(nèi)存通信
我們可以啟用多個單線程的進程并在進程間通信,也可以在單一進程內(nèi)發(fā)動多個線程而在線程間通信,后者的額外開銷更低。因此,即使共享內(nèi)存帶來隱患,主流語言大都青睞以多線程的方式實現(xiàn)并發(fā)功能,當(dāng)中也包括C++。再加上C++本身尚不直接支持進程間通信,所以采用多進程的應(yīng)用軟件將不得不依賴于平臺專屬的應(yīng)用程序接口(Application Program Interface,API)。鑒于此,本書專攻多線程并發(fā),后文再提及并發(fā),便假定采用多線程實現(xiàn)。
提到多線程代碼,還常常用到一個詞——并行。接下來,我們來厘清并發(fā)與并行的區(qū)別。
1.1.3 并發(fā)與并行
就多線程代碼而言,并發(fā)與并行(parallel)的含義很大程度上相互重疊。確實,在多數(shù)人看來,它們就是相同的。二者差別甚小,主要是著眼點和使用意圖不同。兩個術(shù)語都是指使用可調(diào)配的硬件資源同時運行多個任務(wù),但并行更強調(diào)性能。當(dāng)人們談及并行時,主要關(guān)心的是利用可調(diào)配的硬件資源提升大規(guī)模數(shù)據(jù)處理的性能;當(dāng)談及并發(fā)時,主要關(guān)心的是分離關(guān)注點或響應(yīng)能力。這兩個術(shù)語之間并非涇渭分明,它們之間仍有很大程度的重疊,知曉這點會對后文的討論有所幫助,兩者的范例將穿插本書。
至此,我們已明晰并發(fā)的含義,現(xiàn)在來看看應(yīng)用軟件為什么要使用并發(fā)技術(shù)。
1.2 為什么使用并發(fā)技術(shù)
應(yīng)用軟件使用并發(fā)技術(shù)的主要原因有兩個:分離關(guān)注點與性能提升。據(jù)我所知,實際上這幾乎是僅有的用到并發(fā)技術(shù)的原因。如果尋根究底,任何其他原因都能歸結(jié)為二者之一,也可能兼有(除非硬要說“因為我就是想并發(fā)”)。
1.2.1 為分離關(guān)注點而并發(fā)
一直以來,編寫軟件時,分離關(guān)注點(separation of concerns)幾乎總是不錯的構(gòu)思:歸類相關(guān)代碼,隔離無關(guān)代碼,使程序更易于理解和測試,因此所含缺陷很可能更少。并發(fā)技術(shù)可以用于隔離不同領(lǐng)域的操作,即便這些不同領(lǐng)域的操作需同時進行;若不直接使用并發(fā)技術(shù),我們將不得不編寫框架做任務(wù)切換,或者不得不在某個操作步驟中,頻繁調(diào)用無關(guān)領(lǐng)域的代碼。
考慮一個帶有用戶界面的應(yīng)用軟件,需要由CPU密集處理,如臺式計算機上的DVD播放軟件。本質(zhì)上,這個應(yīng)用軟件肩負(fù)兩大職責(zé):既要從碟片讀取數(shù)據(jù),解碼聲音影像,并將其及時傳送給圖形硬件和音效硬件,讓DVD順暢放映,又要接收用戶的操作輸入,譬如用戶按“暫停”“返回選項單”“退出”等鍵。假若采取單一線程,則該應(yīng)用軟件在播放過程中,不得不定時檢查用戶輸入,結(jié)果會混雜播放DVD的代碼與用戶界面的代碼。改用多線程就可以分離上述兩個關(guān)注點,一個線程只負(fù)責(zé)用戶界面管理,另一個線程只負(fù)責(zé)播放DVD,用戶界面的代碼和播放DVD的代碼遂可避免緊密糾纏。兩個線程之間還會保留必要的交互,例如按“暫停”鍵,不過這些交互僅僅與需要立即處理的事件直接關(guān)聯(lián)。
如果用戶發(fā)送了操作請求,而播放DVD線程正忙,無法馬上處理,那么在請求被傳送到該線程的同時,代碼通常能令用戶界面線程立刻做出響應(yīng),即便只是顯示光標(biāo)或提示“請稍候”。這種方法使得應(yīng)用軟件看起來響應(yīng)及時。類似地,某些必須在后臺持續(xù)工作的任務(wù),則常常交由獨立線程負(fù)責(zé)運行,例如,讓桌面搜索應(yīng)用軟件監(jiān)控文件系統(tǒng)變動。此法基本能大幅簡化各線程的內(nèi)部邏輯,原因是線程間交互得以限定于代碼中可明確辨識的切入點,而無須將不同任務(wù)的邏輯交錯散置。
這樣,線程的實際數(shù)量便與CPU既有的內(nèi)核數(shù)量無關(guān),因為用線程分離關(guān)注點的依據(jù)是設(shè)計理念,不以增加運算吞吐量為目的。
1.2.2 為性能而并發(fā):任務(wù)并行和數(shù)據(jù)并行
多處理器系統(tǒng)已存在數(shù)十年,不過一直以來它們大都只見于巨型計算機、大型計算機和大型服務(wù)器系統(tǒng)。但是,芯片廠家日益傾向設(shè)計多核芯片,在單一芯片上集成2個、4個、16個或更多處理器,從而使其性能優(yōu)于單核芯片。于是,多核臺式計算機日漸流行,甚至多核嵌入式設(shè)備亦然。不斷增強的算力并非得益于單個任務(wù)的加速運行,而是來自多任務(wù)并行運作。從前,處理器更新?lián)Q代,程序自然而然隨之加速,程序員可以“坐享其成,不勞而獲”。但現(xiàn)在,正如Herb Sutter指出的“免費午餐沒有了![3]”,軟件若要利用增強的這部分算力,就必須設(shè)計成并發(fā)運行任務(wù)。所以程序員必須警覺,特別是那些躊躇不前、忽視并發(fā)技術(shù)的同業(yè),有必要注意熟練掌握并發(fā)技術(shù),儲備技能。
增強性能的并發(fā)方式有兩種。第一種,最直觀地,將單一任務(wù)分解成多個部分,各自并行運作,從而節(jié)省總運行耗時。此方式即為任務(wù)并行。盡管聽起來淺白、直接,但這卻有可能涉及相當(dāng)復(fù)雜的處理過程,因為任務(wù)各部分之間也許存在紛繁的依賴。任務(wù)分解可以針對處理過程,調(diào)度某線程運行同一算法的某部分,另一線程則運行其他部分;也可以針對數(shù)據(jù),線程分別對數(shù)據(jù)的不同部分執(zhí)行同樣的操作,這被稱為數(shù)據(jù)并行。
易于采用上述并行方式的算法常常被稱為尷尬并行[4]算法。其含義是,將算法的代碼并行化實在簡單,甚至簡單得會讓我們尷尬,實際上這是好事。我還遇見過用其他術(shù)語描述這類算法,叫“天然并行”(naturally parallel)與“方便并發(fā)”(conveniently concurrent)。尷尬并行算法具備的優(yōu)良特性是可按規(guī)模伸縮——只要硬件支持的線程數(shù)目增加,算法的并行程度就能相應(yīng)提升。這種算法是成語“眾擎易舉”的完美體現(xiàn)。算法中除尷尬并行以外的部分,可以另外劃分成一類,其并行任務(wù)的數(shù)目固定(所以不可按規(guī)模伸縮)。第8章和第10章將涵蓋按線程分解任務(wù)的方法。
第二種增強性能的并發(fā)方式是利用并行資源解決規(guī)模更大的問題。例如,只要條件適合,便同時處理2個文件,或者10個,甚至20個,而不是每次1個。同時對多組數(shù)據(jù)執(zhí)行一樣的操作,實際上是采用了數(shù)據(jù)并行,其著眼點有別于任務(wù)并行。采用這種方式處理單一數(shù)據(jù)所需的時間依舊不變,而同等時間內(nèi)能處理的數(shù)據(jù)相對更多。這種方式明顯存在局限,雖然并非任何情形都會因此受益,但數(shù)據(jù)吞吐量卻有所增加,進而帶來突破。例如,若能并行處理視頻影像中不同的區(qū)域,就會提升視頻處理的解析度。
1.2.3 什么時候避免并發(fā)
知道何時避免并發(fā),與知道何時采用并發(fā)同等重要。歸根結(jié)底,不用并發(fā)技術(shù)的唯一原因是收益不及代價。多數(shù)情況下,采用了并發(fā)技術(shù)的代碼更難理解,編寫和維護多線程代碼會更勞心費神,并且復(fù)雜度增加可能導(dǎo)致更多錯誤。編寫正確運行的多線程代碼需要額外的開發(fā)時間和相關(guān)維護成本,除非潛在的性能提升或分離關(guān)注點而提高的清晰度值得這些開銷,否則別使用并發(fā)技術(shù)。
此外,性能增幅可能不如預(yù)期。線程的啟動存在固有開銷,因為系統(tǒng)須妥善分配相關(guān)的內(nèi)核資源和棧空間,然后才可以往調(diào)度器添加新線程,這些都會耗費時間。假如子線程上運行的任務(wù)太快完成,處理任務(wù)本身的時間就會遠(yuǎn)短于線程啟動的時間,結(jié)果,應(yīng)用程序的整體性能很可能還不如完全由主線程直接執(zhí)行任務(wù)的性能。
再者,線程是一種有限的資源。若一次運行太多線程,便會消耗操作系統(tǒng)資源,可能令系統(tǒng)整體變慢。而且,由于每個線程都需要獨立的棧空間[5],如果線程太多,就可能耗盡所屬進程的可用內(nèi)存或地址空間。在采用扁平模式內(nèi)存架構(gòu)的32位進程中,可用的地址空間是4GB,這很成問題:假定每個線程棧的大小都是1MB(這個大小常見于許多系統(tǒng)),那么4096個線程即會把全部地址空間耗盡,使得代碼、靜態(tài)數(shù)據(jù)和堆數(shù)據(jù)無地立足。盡管64位系統(tǒng)(或指令集寬度更大的系統(tǒng))對地址空間的直接限制相對寬松,但其資源依舊有限,運行太多線程仍將帶來問題。雖然線程池可用于控制線程數(shù)量(見第9章),但也非萬能妙法,它自身也有局限。
假設(shè),在服務(wù)器端,客戶端/服務(wù)器(Client/Server,C/S)模式的應(yīng)用程序為每個連接發(fā)起一個獨立的線程。如果只有少量連接,這尚能良好工作。不過,請求量巨大的服務(wù)器需要處理的連接數(shù)目龐大,若采用同樣的方法,就會發(fā)起過多線程而很快耗盡系統(tǒng)資源。針對這一情形,如果要達到最優(yōu)性能,便須謹(jǐn)慎使用線程池(見第9章)。
最后,運行的線程越多,操作系統(tǒng)所做的上下文切換就越頻繁,每一次切換都會減少本該用于實質(zhì)工作的時間。結(jié)果,當(dāng)線程數(shù)目達到一定程度時,再增加新線程只會降低應(yīng)用軟件的整體性能,而不會提升性能。正因如此,若讀者意在追求最優(yōu)系統(tǒng)性能,則須以可用的硬件并發(fā)資源為依據(jù)(或反之考慮其匱乏程度),調(diào)整運行線程的數(shù)目。
為了提升性能而使用并發(fā)技術(shù),與其他優(yōu)化策略相仿:它極具提升應(yīng)用程序性能的潛力,卻也可能令代碼復(fù)雜化,使之更難理解、更容易出錯。所以,對于應(yīng)用程序中涉及性能的關(guān)鍵部分,若其具備提升性能的潛力,收效可觀,才值得為之實現(xiàn)并發(fā)功能。當(dāng)然,如果首要目標(biāo)是設(shè)計得清楚明晰或分離關(guān)注點,而提升性能居次,也值得采用多線程設(shè)計。
倘若讀者已決意在應(yīng)用軟件中使用并發(fā)技術(shù),不論是為了性能,還是為了分離關(guān)注點,或是因為“多線程的良辰吉日已到”,那么這對C++程序員意義何在?
1.3 并發(fā)與C++多線程
以標(biāo)準(zhǔn)化形式借多線程支持并發(fā)是C++的新特性。C++11標(biāo)準(zhǔn)發(fā)布后,我們才不再依靠平臺專屬的擴展,可以用原生C++直接編寫多線程代碼。標(biāo)準(zhǔn)C++線程庫的成型歷經(jīng)種種取舍,若要掌握其設(shè)計邏輯,則知曉其歷史淵源頗為重要。
1.3.1 C++多線程簡史
1998年發(fā)布的C++標(biāo)準(zhǔn)并沒有采納線程,許多語言要素在設(shè)定其操作效果之時,考慮的依據(jù)是抽象的串行計算機(sequential abstract machine)[6]。不僅如此,內(nèi)存模型亦未正式定義,若不依靠編譯器相關(guān)的擴展填補C++98標(biāo)準(zhǔn)的不足,就無法寫出多線程程序。
為了支持多線程,編譯器廠商便自行對C++進行擴展;廣泛流行的C語言的多線程API,如符合POSIX規(guī)范的C語言多線程接口[7]和微軟Windows系統(tǒng)的多線程API,使得許多C++廠商借助各種平臺專屬的擴展來支持多線程。這種來自編譯器的支持普遍受限,在特定平臺上只能使用相應(yīng)的C語言API,并且須確保C++運行庫(譬如異常處理機制的代碼)在多線程場景下可以正常工作。盡管甚少編譯器廠商給出了正規(guī)的多線程內(nèi)存模型,但編譯器和處理器運作優(yōu)良,使數(shù)量龐大的多線程程序得以用C++寫就。
C++程序員并不滿足于使用平臺專屬的C語言API處理多線程,他們更期待C++類庫提供面向?qū)ο蟮亩嗑€程工具。應(yīng)用程序框架(如微軟基礎(chǔ)類庫[8])和通用的C++程序庫(如Boost和自適配通信環(huán)境[9])已累積開發(fā)了多個系列的C++類,封裝了平臺專屬的底層API,并提供高級的多線程工具以簡化編程任務(wù)。盡管C++類庫的具體細(xì)節(jié)千差萬別,特別是在啟動新線程這一方面,但這些C++類的總體特征有很多共同之處。例如,通過資源獲取即初始化(Resource Acquisition Is Initialization,RAII)的慣用手法進行鎖操作,它確保了一旦脫離相關(guān)作用域,被鎖的互斥就自行解開。這項設(shè)計特別重要,為許多C++類庫所共有,使程序員受益良多。
現(xiàn)有的C++編譯器在許多情況下都能支持多線程,再結(jié)合平臺專屬的API以及平臺無關(guān)的類庫,如Boost和ACE,為編寫多線程的C++代碼奠定了堅實的基礎(chǔ)。于是,無數(shù)多線程應(yīng)用的組件由C++寫成,代碼量龐大,以百萬行計。不過它們?nèi)狈y(tǒng)一標(biāo)準(zhǔn)的支持,這意味著,由于欠缺多線程內(nèi)存模型,因此在某些情形下程序會出現(xiàn)問題,下面兩種情形尤甚:依賴某特定的處理器硬件架構(gòu)來獲得性能提升,或是編寫跨平臺代碼,但編譯器的行為卻因平臺而異。
1.3.2 新標(biāo)準(zhǔn)對并發(fā)的支持
隨著C++11標(biāo)準(zhǔn)的發(fā)布,上述種種弊端被一掃而空。C++標(biāo)準(zhǔn)庫不僅規(guī)定了內(nèi)存模型,可以區(qū)分不同線程,還擴增了新類,分別用于線程管控(見第2章)、保護共享數(shù)據(jù)(見第3章)、同步線程間操作(見第4章)以及底層原子操作(見第5章)等。
前文提及的幾個C++類庫在過往被使用過程中積累了很多經(jīng)驗,C++11線程庫對它們頗為倚重。具體而言,新的C++庫以Boost線程庫作為原始范本,其中很多類在Boost線程庫中存在對應(yīng)者,名字和結(jié)構(gòu)均一致。另外,Boost線程庫自身做了多方面改動,以遵循C++標(biāo)準(zhǔn)。因此,原來的Boost使用者應(yīng)該會對標(biāo)準(zhǔn)C++線程庫深感熟悉。
正如本章開篇所述,C++11標(biāo)準(zhǔn)進行了多項革新,支持并發(fā)特性只是其中之一,語言自身還有很多的改進,以便程序員揮灑自如。雖然這些改進普遍超出本書范圍,但其中一部分直接影響了C++線程庫本身及其使用方式。附錄A將簡要介紹這些C++新特性。
1.3.3 C++14和C++17進一步支持并發(fā)和并行
C++14進一步增添了對并發(fā)和并行的支持,具體而言,是引入了一種用于保護共享數(shù)據(jù)的新互斥(見第3章)。C++17則增添了一系列適合新手的并行算法函數(shù)(見第10章)。這兩版標(biāo)準(zhǔn)都強化了C++的核心和標(biāo)準(zhǔn)程序庫的其他部分,簡化了多線程代碼的編寫。
如前文所述,C++標(biāo)準(zhǔn)委員會還發(fā)布了并發(fā)技術(shù)規(guī)約,詳述了對C++標(biāo)準(zhǔn)提供的類和函數(shù)所做的擴展,特別是有關(guān)線程間的同步操作。
C++明確規(guī)定了原子操作的語義,并予以直接支持,使開發(fā)者得以擺脫平臺專屬的匯編語言,僅用純C++即可編寫出高效的代碼。對于力求編寫高效且可移植的代碼的開發(fā)者,這簡直如有神助:不但由編譯器負(fù)責(zé)處理平臺的底層細(xì)節(jié),還能通過優(yōu)化器把操作語義也考慮在內(nèi),兩者聯(lián)手改進性能,使程序的整體優(yōu)化效果更上一層樓。
1.3.4 標(biāo)準(zhǔn)C++線程庫的效率
通常,對于從事高性能計算工作的開發(fā)者,無論是從整體上考量C++,還是就封裝了底層工具的C++類而言(具體來說,以新的標(biāo)準(zhǔn)C++線程庫中的類為例),他們最在意的因素通常是運行效率。若要實現(xiàn)某項功能,代碼可以借助高級工具,或者直接使用底層工具。兩種方式的運行開銷不同,該項差異叫作抽象損失[10]。如果讀者追求極致性能,清楚這點便尤為重要。
在設(shè)計C++標(biāo)準(zhǔn)庫和標(biāo)準(zhǔn)C++線程庫時,C++標(biāo)準(zhǔn)委員會對此非常注意。其中一項設(shè)計目標(biāo)是,假定某些代碼采用了C++標(biāo)準(zhǔn)庫所提供的工具,如果改換為直接使用底層API,應(yīng)該不會帶來性能增益,或者收效甚微。因此,在絕大多數(shù)主流平臺上,C++標(biāo)準(zhǔn)庫得以高效地實現(xiàn)(低抽象損失)。
總有開發(fā)者追求性能極限,恨不得下探最底層,親手掌控半導(dǎo)體器件,以窮盡計算機的算力。C++標(biāo)準(zhǔn)委員會的另一個目標(biāo)是,確保C++提供充足的底層工具來滿足需求。為此,新標(biāo)準(zhǔn)帶來了新的內(nèi)存模型,以及全方位的原子操作庫,其能直接單獨操作每個位、每個字節(jié),還能直接管控線程同步,并讓線程之間可以看見數(shù)據(jù)變更。過去,開發(fā)者若想深入底層,就得選用平臺專屬的匯編語言;現(xiàn)在,在許多場合,這些原子型別和對應(yīng)的操作都足以取而代之。只要代碼采用了新標(biāo)準(zhǔn)的數(shù)據(jù)型別與操作,便更具可移植性,且更容易維護。
C++標(biāo)準(zhǔn)庫還提供了高級工具,抽象程度更高,更易于編寫多線程代碼,出錯機會更少。使用這些工具必須執(zhí)行額外的代碼[11],所以有時確實會增加性能開銷,但這種性能開銷不一定會引發(fā)更多抽象損失。與之相比,實現(xiàn)同樣的功能,手動編寫代碼所產(chǎn)生的開銷往往更高。此外,對于上述絕大部分額外的代碼,編譯器會妥善進行內(nèi)聯(lián)。
針對某種特定的使用需求,一些高級工具提供了所需功能,有時還給出了額外功能,超出了原本的需求。在大多情況下,這都不成問題:未被使用的功能完全不產(chǎn)生開銷。只有在極少數(shù)情況下,這些未被使用的功能會影響其他代碼的性能。若讀者追求卓越性能,無奈高級工具的開銷過大,那最好還是利用底層工具親自編寫所需功能。在絕大部分情況中,這將導(dǎo)致復(fù)雜度和出錯的可能性同時大增,而性能提升卻十分有限,得益遠(yuǎn)遠(yuǎn)不償所失。有時候,即便性能剖析表明瓶頸在于C++標(biāo)準(zhǔn)庫的工具,但根本原因還是應(yīng)用程序設(shè)計失當(dāng),而非類庫的實現(xiàn)欠佳。譬如,如果太多線程爭搶同一個互斥對象,就會嚴(yán)重影響性能。與其試圖壓縮互斥操作以節(jié)省瑣碎時間,不如重新構(gòu)建應(yīng)用程序,從根本上減少對互斥對象的爭搶,收效很可能更明顯。第8章會涵蓋上述議題:如何設(shè)計并發(fā)應(yīng)用,減少資源爭搶。
C++標(biāo)準(zhǔn)庫還是有可能無法達到性能要求,無法提供所需的功能,但這種情況非常少,一旦出現(xiàn)這種情況,就似乎有必要使用平臺專屬的工具了。
1.3.5 平臺專屬的工具
雖然標(biāo)準(zhǔn)C++線程庫給出了相當(dāng)全面的多線程和并發(fā)工具,但在任何特定的平臺上,總有平臺專屬的工具超額提供標(biāo)準(zhǔn)庫之外的功能。為了可以便捷利用這些工具,同時又能照常使用標(biāo)準(zhǔn)C++線程庫,C++線程庫的某些型別有可能提供成員函數(shù)native_handle(),允許它的底層直接運用平臺專屬的API。因其本質(zhì)使然,任何采用native_handle()的操作都完全依賴于特定平臺,這也超出了本書的范圍(以及C++標(biāo)準(zhǔn)庫自身的范圍)。
總結(jié)
以上是生活随笔為你收集整理的有什么好的并发编程书籍推荐?还真有一本的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 解决迅雷、酷我软件安装乱码问题
- 下一篇: Mac OS 使用远程桌面登录服务器