本文是《C++ 併發編程》的第一章,感謝人民郵電出版社受權併發編程網發表此文,版權全部,請勿轉載。該書將於近期上市。程序員
本章主要內容web
- 何謂併發和多線程
- 爲何要在應用程序中使用併發和多線程
- C++併發支持的發展歷程
- 一個簡單的C++多線程程序是什麼樣的
這是C++用戶的振奮時刻。距1998年初始的C++標準發佈13年後,C++標準委員會給予程序語言和它的支持庫一次重大的變革。新的C++標準(也被稱爲C++11或C++0x)於2011年發佈並帶來了不少的改變,使得C++的應用更加容易並富有成效。算法
在C++11標準中一個最重要的新特性就是支持多線程程序。這是C++標準第一次在語言中認可多線程應用的存在,並在庫中爲編寫多線程應用程序提供組件。這將使得在不依賴平臺相關擴展下編寫多線程C++程序成爲可能,從而容許以有保證的行爲來編寫可移植的多線程代碼。這也恰逢程序員更多地尋求廣泛的併發,特別是多線程程序,來提升應用程序的性能。編程
這本書講述的就是在C++編程中對多線程併發的使用,以及令其成爲可能的C++語言特性和庫工具。我會以解釋併發和多線程的含義以及爲何要在應用程序中使用併發開始。在快速繞行闡述爲何在應用程序中會不使用併發以後,我會對C++中併發支持進行概述,並以一個簡單的C++併發實例結束這一章。具備開發多線程應用程序經驗的讀者能夠跳過前面的小節。在隨後幾章會將涵蓋更多普遍的例子,而且更深刻地瞭解庫工具。本書最後附有多線程與併發所有的C++標準庫工具的深刻參考。瀏覽器
那麼,什麼是併發(concurrency)和多線程(multithreading)?緩存
1.1 什麼是併發
在最簡單和最基本的層面,併發是指兩個或更多獨立的活動同時發生。併發在生活中隨處可見;咱們能夠一邊走路一邊說話,也能夠兩隻手同時做不一樣的動做,還有咱們每一個人都相互獨立地過咱們的生活——我在游泳的時候你能夠看球賽,等等。安全
1.1.1 計算機系統中的併發
當咱們提到計算機術語的併發,咱們指的是在單個系統裏同時執行多個獨立的活動,而不是順序地或是一個接一個地。這並非新現象,多任務操做系統經過任務切換容許一臺計算機在同一時間運行多個應用程序已司空見慣多年,一些高端的多處理器服務器啓用真正的併發的時間更爲長久。真正有新意的是增長計算機真正並行運行多任務的廣泛性,而不僅是給人這種錯覺。服務器
之前,大多數計算機都有一個處理器,具備單個處理單元或核心,至今許多臺式機器還是這樣。這種計算機在某一時刻只能夠真正執行一個任務,但它能夠每秒切換任務許屢次。經過作一點這個任務而後再作一點別的任務,看起來像是任務在並行發生。這就是任務切換(task switching)。咱們仍然將這樣的系統論爲併發(concurrency),由於任務切換得太快,以致於沒法分辨任務在什麼時候會被暫掛而切換到另外一個任務。任務切換給用戶和應用程序自己提供了一種併發的假象。因爲這只是併發的假象,當應用程序執行在單處理器任務切換環境下,與執行在真正的併發環境下相比,其行爲仍是有着微妙的不一樣。特別地,對內存模型不正確的假設(詳見第5章)在這樣的環境中可能不會出現。這將在第10章中做深刻討論。網絡
包含多個處理器的計算機用於服務器和高性能計算任務已有多年,如今基於單個芯片上具備多於一個核心的處理器(多核心處理器)的計算機也成爲愈來愈常見的臺式機器。不管它們擁有多個處理器或一個多核處理器(或二者兼具),這些計算機可以真正的並行運行超過一個任務。咱們稱之爲硬件併發(hardware concurrency)。
圖1.1顯示了一個計算機處理剛好兩個任務時的理想情景,每一個任務被分爲10個相等大小的塊。在一個雙核機器(具備兩個處理核心)中,每一個任務能夠在各自的核心執行。在單核機器上作任務切換時,每一個任務的塊交織進行。但它們也隔開了一位(圖中所示灰色分隔條的厚度大於雙核機器的分隔條);爲了實現交織進行,該系統每次從一個任務切換到另外一個時都得執行一次上下文切換(context switch),而這是須要時間的。爲了執行上下文切換,操做系統必須得爲當前運行的任務保存CPU的狀態和指令指針,算出要切換到哪一個任務,併爲要切換到的任務從新加載處理器狀態。而後CPU可能要將新任務的指令和數據的內存載入到緩存中,這可能阻止CPU執行任何指令,形成的進一步延遲。
圖 1.1併發的兩種方式:雙核機器的並行執行對比單核機器的任務切換
儘管硬件併發的可用性在多處理器或多核系統上更顯著,有些處理器卻能夠在一個核心上執行多個線程。要考慮的最重要的因素是硬件線程(hardware threads)的數量:即硬件能夠真正併發運行多少獨立的任務。即使是具備真正硬件併發的系統,也很容易有超過硬件可並行運行的任務要執行,因此在這些狀況下任務切換仍將被使用。例如,在一個典型的臺式計算機上可能會有幾百個的任務在運行,執行後臺操做,即便在計算機名義上是空閒的。正是任務切換使得這些後臺任務能夠運行,並使得你能夠同時運行文字處理器、編譯器、編輯器和web瀏覽器(或任何應用的組合)。圖1.2顯示了四個任務在一臺雙核機器上的任務切換,仍然是將任務整齊地劃分爲同等大小塊的理想狀況。實際上,許多因素會使得分割不均和調度不規則。這些因素中的一部分將涵蓋在第8章中,那時咱們來看一看影響並行代碼性能的因素。
全部的技術、功能和本書所涉及的類均可以被使用,不管你的應用程序是在單核處理器或多核處理器上運行,也無論是任務切換或是真正的硬件併發。但你能夠想象,如何在你的應用程序中使用併發將很大程度上取決於可用的硬件併發。這將在第8章中涵蓋,在那裏咱們具體研究C++代碼並行設計問題。
圖 1.2四個任務在兩個核心之間的切換
1.1.2 併發的途徑
想象一下兩個程序員一塊兒作一個軟件項目。若是你的開發人員在獨立的辦公室,它們能夠各自平靜地工做,而不會互相干擾,而且他們各有本身的一套參考手冊。然而,溝通起來就不那麼直接了;不能轉身而後互相交談,他們必須用電話、電子郵件或走到對方的辦公室。同時,你須要掌控兩個辦公室的開銷,還要購買多份參考手冊。
如今想象一下把開發人員移到同一間辦公室。他們如今能夠地相互交談來討論應用程序的設計,他們也能夠很容易的用紙或白板來繪製圖表,輔助闡釋設計思路。你如今只有一個辦公室要管理,只要一組資源就能夠知足。消極的一面是,他們可能會發現難以集中注意力,而且還可能存在資源共享的問題(「參考手冊跑哪去了?」)
組織開發人員的這兩種方法表明着併發的兩種基本途徑。每一個開發人員表明一個線程,每一個辦公室表明一個處理器。第一種途徑是有多個單線程的進程,這就相似讓每一個開發人員在他們本身的辦公室,而第二種途徑是在單一進程裏有多個線程,這就相似在同一個辦公室裏有兩個開發人員。你能夠隨意進行組合,而且擁有多個進程,其中一些是多線程的,一些是單線程的,但原理是同樣的。讓咱們在一個應用程序中簡要地看一看這兩種途徑。
多進程併發
在一個應用程序中使用併發的第一種方法,是將應用程序分爲多個、獨立的、單線程的進程,它們運行在同一時刻,就像你能夠同時進行網頁瀏覽和文字處理。這些獨立的進程能夠經過全部的常規的進程間通訊渠道互相傳遞消訊息(信號、套接字、文件、管道等等),如圖1.3所示。有一個缺點是這種進程之間的通訊一般設置複雜,或是速度較慢,或二者兼備,由於操做系統一般在進程間提供了大量的保護,以免一個進程不當心修改了屬於另外一個進程的數據。另外一個缺點是運行多個進程所需的固有的開銷:啓動進程須要時間,操做系統必須投入內部資源來管理進程,等等。
固然,也並不全是缺點:操做系統在線程間提供的附加保護操做和更高級別的通訊機制,意味着能夠比線程更容易地編寫安全的併發代碼。事實上,相似於爲Erlang編程語言提供的環境,使用進程做爲重大做用併發的基本構造快。
使用獨立的進程實現併發還有一個額外的優點——你能夠在經過網絡鏈接的不一樣的機器上運行的獨立的進程。雖然這增長了通訊成本,但在一個精心設計的系統上,它多是一個提升並行可用行和提升性能的低成本方法。
圖 1.3一對併發運行的進程之間的通訊
多線程併發
併發的另外一個途徑是在單個進程中運行多個線程。線程很像輕量級的進程:每一個線程相互獨立運行,且每一個線程能夠運行不一樣的指令序列。但進程中的全部線程都共享相同的地址空間,而且從全部線程中訪問到大部分數據——全局變量仍然是全局的,指針、對象的引用或數據能夠在線程之間傳遞。雖然一般能夠在進程之間共享內存,但這難以創建而且一般難以管理,由於同一數據的內存地址在不一樣的進程中也不盡相同。圖1.4顯示了一個進程中的兩個線程經過共享內存進行通訊。
圖 1.4同一進程中的一對併發運行的線程之間的通訊
共享的地址空間,以及缺乏線程間的數據保護,使得使用多線程相關的開銷遠小於使用多個進程,由於操做系統有更少的簿記要作。可是,共享內存的靈活性是有代價的:若是數據要被多個線程訪問,那麼程序員必須確保當每一個線程訪問時所看到的數據是一致的。線程間數據共享可能會遇到的問題、所使用的工具以及爲了不問題而要遵循的指導方針在本書中都有涉及,特別是在第三、四、5和8章中。這些問題並不是不可克服,只要在編寫代碼時適當地注意便可,但這卻意味着必須對線程之間的通訊做大量的思考。
相比於啓動多個單線程進程並在其間進行通訊,啓動單一進程中的多線程並在其間進行通訊的開銷更低,這意味着若不考慮共享內存可能會帶來的潛在問題,它是包括C++在內的主流語言更青睞的併發途徑。此外,C++標準沒有爲進程間通訊提供任何原生支持,因此使用多進程的應用程序將不得不依賴平臺相關的API來實現。所以,本書專門關注使用多線程的併發,而且以後提到併發均是假定經過使用多線程來實現的。
明確了什麼是併發後,如今讓咱們來看看爲何要在應用程序中使用併發。
1.2 爲何使用併發?
在應用程序中使用併發的緣由主要有兩個:關注點分離和性能。事實上,我甚至能夠說它們差很少是使用併發的惟一緣由;當你觀察的足夠仔細時,一切其餘因素均可以歸結到這二者之一(或者多是兩者兼有,固然,除了像「由於我願意」這樣的緣由以外)。
1.2.1 爲了關注點分離而使用併發
在編寫軟件時,關注點分離幾乎老是個好主意;經過將相關的代碼放在一塊兒並將無關的代碼分開,可使你的程序更容易理解和測試,從而減小出錯的可能性。你可使用併發來分隔不一樣的功能區域,即便在這些不一樣功能區域的操做須要在同一時刻發生的窮況下;若不顯式地使用併發,你要麼被迫編寫任務切換框架,要麼在操做中主動地調用不相關的一段代碼。
考慮一類帶有用戶界面的密集處理型應用程序,例如爲臺式計算機提供的DVD播放程序。這樣一個應用程序基本上具有兩套職能:它不只要從光盤中讀取數據,解碼圖像和聲音,並把它們及時輸出至視頻和音頻硬件,從而實現DVD的無錯播放;它還要接受來自用戶的輸入,例如當用戶單擊暫停或返回菜單甚至退出按鍵的時候。在單個線程中,應用程序須在回放期間按期檢查用戶的輸入,因而將將DVD回放代碼和用戶界面代碼合在一塊兒。經過使用多線程來分隔這些關注點,用戶界面代碼和DVD回放代碼再也不須要如此緊密的交織在一塊兒;一個線程能夠處理用戶界面,另外一個處理DVD回放。它們之間會有交互,例如用戶點擊暫停,但如今這些交互直接與眼前的任務有關。
這會帶來響應性的錯覺,由於用戶界面線程一般能夠當即響應用戶的請求,即便在請求被傳達給幹活的線程時,響應爲簡單地顯示正忙的光標或請等待的消息。相似地,獨立的線程常被用於運行必須在後臺連續運行的任務,例如在桌面搜索程序中監視文件系統的變化。以這種方式使用線程通常會使每一個線程的邏輯更加簡單,由於它們之間的交互能夠被限制爲清晰可辨的點,而不是處處散播不一樣任務的邏輯。
在這種狀況下,線程的數量與CPU可用內核的數量無關,由於對線程的劃分是基於概念上的設計而不是試圖增長吞吐量。
1.2.2 爲了性能而使用併發
多處理器系統已經存在了幾十年,但直到最近,他們幾乎只能在超級計算機、大型機和大型服務器系統中才能看到。然而芯片製造商愈來愈傾向於多核芯片的設計,即在單個芯片上集成二、四、16或更多的處理器,從而達到比單核心更好的性能。所以,多核臺式計算機,甚至多核嵌入式設備,如今愈來愈廣泛。這些計算機的計算能力的提升不是源自使單一任務運行的更快,而是源自並行運行多個任務。在過去,程序員曾坐看他們的程序隨着處理器的更新換代而變得更快,無需他們這邊作出任何努力。可是如今,就像Herb Sutter所說的,「免費的午飯結束了。」[1]若是軟件想要利用日益增加的計算能力,它必須設計爲併發運行多個任務。程序員所以必須留意,並且那些迄今都忽略併發的人們在必須注意它並將其加入他們的工具箱中。
有兩種方式爲了性能使用併發。首先,也是最明顯的,是將一個單個任務分紅幾部分且各自並行運行,從而下降總運行時間。這就是任務並行(task parallelism)。雖然這聽起來很直觀,但它能夠是一個至關複雜的過程,由於在各個部分之間可能存在不少的依賴。區別多是在過程方面——一個線程執行算法的一部分而另外一個線程執行算法的另外一個部分——或是在數據方面——每一個線程在不一樣的數據部分上執行相同的操做。後一種方法被稱爲數據並行(data parallelism)。
容易受這種並行影響的算法常被稱爲易並行(embarrassingly parallel)。拋開你可能會尷尬地面對很容易並行化的代碼這一含義,這是一件好事情:我曾遇到過的關於此算法的別的術語是天然並行(naturally parallel)和便利併發(conveniently concurrent)。易並行算法具備良好的可擴展特性——隨着可用硬件線程數量的提高,算法的並行性能夠隨之增長與之匹配。這樣的一個算法是諺語「人多力量大」的完美體現。對於非易並行算法的那一部分,你能夠將算法劃分爲一個固定(於是不可擴展)數量的並行任務。在線程之間劃分任務的技巧涵蓋在第8章中。
使用併發來提高性能的第二種方法是使用可用的並行方式來解決更大的問題;與其同時處理一個文件,不如酌情處理2個或10個或20個。雖然這實際上只是數據並行的一種應用,經過對多組數據同時執行相同的操做,但仍是有不一樣的重點。處理一個數據塊仍然須要一樣的時間,但在相同的時間內卻能夠處理更多的數據。固然,這種方法也存在限制,且並不是在全部狀況下都是有益的,可是這種方法所帶來的吞吐量提高可讓一些新玩意變得可能,例如,若是圖片的各部分能夠並行處理,就能提升視頻處理的分辨率。
1.2.3 何時不使用併發
知道什麼時候不使用併發與知道什麼時候使用它同樣重要。基本上,不使用併發的惟一緣由就是在收益比不上成本的時候。使用併發的代碼在不少狀況下難以理解,所以編寫和維護的多線程代碼就有直接的腦力成本,同時額外的複雜性也可能致使更多的錯誤。除非潛在的性能增益足夠大或關注點分離地足夠清晰,能抵消確保其正確所需的額外的開發時間以及與維護多線程代碼相關的額外成本,不然不要使用併發。
一樣地,性能增益可能不會如預期的那麼大;在啓動線程時存在固有的開銷,由於操做系統必須分配相關的內核資源和堆棧空間,而後將新線程加入調度器中,全部這一切都佔用時間。若是在線程上運行的任務完成得很快,那麼任務實際上佔據的時間與啓動線程的開銷時間相比顯得微不足道,可能會致使應用程序的總體性能還不如經過產生線程直接執行該任務。
此外,線程是有限的資源。若是讓太多的線程同時運行,則會消耗操做系統資源,而且使得操做系統總體上運行得更緩慢。不只如此,運行太多的線程會耗盡進程的可用內存或地址空間,由於每一個線程都須要一個獨立的堆棧空間。對於一個可用地址空間限制爲4GB的扁平架構的32位進程來講,這尤爲是個問題:若是每一個線程都有一個1MB的堆棧(對於不少系統來講是典型的),那麼4096個線程將會用盡全部地址空間,再也不爲代碼、靜態數據或者堆數據留有空間。雖然64位(或者更大)的系統不存在這種直接的地址空間限制,它們仍然只具有有限的資源:若是你運行太多的線程,最終會致使問題。儘管線程池(參見第9章)能夠用來限制線程的數量,但這並非靈丹妙藥,它們也有它們本身的問題。
若是客戶端/服務器應用程序的服務器端爲每個連接啓動一個獨立的線程,對於少許的連接是能夠正常工做的,但當一樣的技術用於須要處理大量連接的高需求服務器時,就會由於啓動太多線程而迅速耗盡系統資源。在這種場景下,謹慎地使用線程池能夠提供優化的性能(參見第9章)。
最後,運行越多的線程,操做系統就須要作越多的上下文切換。每一個上下文切換都須要耗費本能夠花在有價值工做上的時間,因此在某些時候,增長一個額外的線程實際上會下降而不是提升應用程序的總體性能。爲此,若是你試圖獲得系統的最佳性能,考慮可用的硬件併發(或缺少之)並調整運行線程的數量是必需的。
爲了性能而使用併發就像全部其餘優化策略同樣:它擁有極大提升應用程序性能的潛力,但它也可能使代碼複雜化,使其更難理解和更容易出錯。所以,只有對應用程序中的那些具備顯著增益潛力的性能關鍵部分才值得這樣作。固然,若是性能收益的潛力僅次於設計清晰或關注點分離,可能也值得使用多線程設計。
假設你已經決定確實要在應用程序中使用併發,不管是爲了性能、關注點分離,或是由於「多線程星期一」,對於C++程序員來講意味着什麼?
1.3 在C++中使用併發和多線程
經過多線程爲併發提供標準化的支持對C++來講是新鮮事物。只有在即將到來的C++11標準中,你才能不依賴平臺相關的擴展來編寫多線程代碼。爲了理解新版本C++線程庫中衆多規則背後的基本原理,瞭解其歷史是很重要的。
1.3.1 C++多線程歷程
1998 C++標準版不認可線程的存在,而且各類語言要素的操做效果都以順序抽象機的形式編寫。不只如此,內存模型也沒有被正式定義,因此對於1998 C++標準,你沒辦法在缺乏編譯器相關擴展的狀況下編寫多線程應用程序。
固然,編譯器供應商能夠自由地向語言添加擴展,而且針對多線程的C API的流行——例如在POSIX C和Microsoft Windows API中的那些——致使不少C++編譯器供應商經過各類平臺相關的擴展來支持多線程。這種編譯器支持廣泛地受限於只容許使用該平臺相應的C API以及確保該C++運行時庫(例如異常處理機制的代碼)在多線程存在的狀況下運行。儘管極少有編譯器供應商提供了一個正式的多線程感知內存模型,但編譯器和處理器的實際表現也已經足夠好,以致於大量的多線程的C++程序已被編寫出來。
因爲不知足於使用平臺相關的C API來處理多線程,C++程序員曾指望他們的類庫提供面向對象的多線程工具。像MFC這樣的應用程序框架,以及像Boost和ACE這樣的C++通用C++類庫曾積累了多套C++類,封裝了下層的平臺相關API並提供高級的多線程工具以簡化任務。各種庫的具體細節,特別是在啓動新線程的方面,存在很大差別,可是這些類的整體構造存在不少共通之處。有一個爲許多C++類庫共有的,同時也是爲程序員提供很大便利的特別重要的設計,就是帶鎖的資源得到即初始化(RAII, Resource Acquisition Is Initialization)的習慣用法,來確保當退出相關做用域的時候互斥元被解鎖。
許多狀況下,現有的C++編譯器所提供的多線程支持,例如Boost和ACE,綜合了平臺相關API以及平臺無關類庫的可用性,爲編寫多線程C++代碼提供一個堅實的基礎,也所以大約有數百萬行C++代碼做爲多線程應用程序的一部分而被編寫出來。但缺少標準的支持,意味着存在缺乏線程感知內存模型從而致使問題的場合,特別是對於那些試圖經過使用處理器硬件能力來獲取更高性能,或是編寫跨平臺代碼可是在不一樣平臺之間編譯器的實際表現存在差別的狀況。
1.3.2 新標準中的併發支持
全部這些都隨着新的C++11標準的發佈而改變了。不只有了一個全新的線程感知內存模型,C++標準庫也被擴展了,包含了用於管理線程(參見第2章)、保護共享數據(參見第3章)、線程間同步操做(參見第4章)以及低級原子操做(參見第5章)的各個類。
新的C++線程庫很大程度上基於以前經過使用上文提到的C++類庫而積累的經驗。特別地,Boost線程庫被用做新類庫所基於的主要模型,不少類與Boost中的對應者共享命名和結構。在新標準演進的過程當中,這是個雙向流動,Boost線程庫也改變了本身,以便在多個方面匹配C++標準,所以從Boost遷移過來的用戶將會發現本身很是習慣。
正如本章開篇提到的那樣,對併發的支持僅僅是新C++標準的變化之一,此外還存在不少對於編程語言自身的改善,可使得程序員們的工做更便捷。這些內容雖然不在本書的論述範圍以內,可是其中的一些變化對於線程庫自己及其使用方式已經造成了直接的衝擊。附錄A對這些語言特性作了簡要的介紹。
C++中對原子操做的直接支持,容許程序員編寫具備肯定語義的高效代碼,而無需平臺相關的彙編語言。這對於那些試圖編寫高效的、可移植代碼的程序員們來講是一個真正的福利;不只有編譯器能夠搞定平臺的具體內容,還能夠編寫優化器來考慮操做的語義,從而讓程序做爲一個總體獲得更好的優化。
1.3.3 C++線程庫的效率
對於C++總體以及包含低級工具的C++類——特別是在新版C++線程庫裏的那些,參與高性能計算的開發者經常關注的一點就是效率。若是你正尋求極致的性能,那麼理解與直接使用底層的低級工具相比,使用高級工具所帶來的實現成本,是很重要的。這個成本就是抽象懲罰(abstraction penalty)。
C++標準委員會在總體設計C++標準庫以及專門設計標準C++線程庫的時候,就已經十分注重這一點了;其設計的目標之一就是在提供相同的工具時,經過直接使用低級API就幾乎或徹底得不到任何好處。所以該類庫被設計爲在大部分主要平臺上都能高效實現(帶有很是低的抽象懲罰)。
C++標準委員會的另外一個目標,是確保C++能提供足夠的低級工具給那些但願與硬件工做得更緊密的程序員,以獲取終極性能。爲了達到這個目的,伴隨着新的內存模型,出現了一個全面的原子操做庫,用於直接控制單個位、字節、線程間同步以及全部變化的可見性。這些原子類型和相應的操做如今能夠在不少地方加以使用,而這些地方之前一般被開發者選擇下放到平臺相關的彙編語言中。使用了新的標準類型和操做的代碼於是具備更佳的可移植性,而且更易於維護。
C++標準庫也提供了更高級別的抽象和工具,它們使得編寫多線程代碼更簡單和不易出錯。有時候運用這些工具確實會帶來性能成本,由於必須執行額外的代碼。可是這種性能成本並不必定意味着更高的抽象懲罰;整體來看,這種性能成本並不比經過手工編寫等效的函數而招致的成本更高,同時編譯器可能會很好地內聯大部分額外的代碼。
在某些狀況下,高級工具提供超出特定使用需求的額外功能。在大部分狀況下這都不是問題:你沒有爲你不使用的那部分買單。在罕見的狀況下,這些未使用的功能會影響其餘代碼的性能。若是你更看重程序的性能,且代價太高,你可能最好是經過較低級別的工具來手工實現須要的功能。在絕大多數狀況下,額外增長的複雜性和出錯的概率遠大於小小的性能提高帶來的潛在收益。即便有證據確實代表瓶頸出如今C++標準庫的工具中,這也可能歸咎於低劣的應用程序設計而非低劣的類庫實現。例如,若是過多的線程競爭一個互斥元,這將會顯著影響性能。與其試圖在互斥操做上刮掉一點點的時間,還不如從新構造應用程序以減小互斥元上的競爭來的划算。設計應用程序以減小競爭會在第8章中加以闡述。
在很是罕見的狀況下,C++標準庫不提供所需的性能或行爲,這時則有必要運用使用平臺相關的工具。
1.3.4 平臺相關的工具
雖然C++線程庫爲多線程和併發處理提供了頗爲全面的工具,可是在全部的平臺上,都會有些額外的平臺相關工具。爲了能方便的訪問那些工具而又不用放棄使用標準C++線程庫帶來的好處,C++線程庫中的類型能夠提供一個native_handle()成員函數,容許經過使用平臺相關API直接操做底層實現。就其本質而言,任何使用native_handle()執行的操做是徹底依賴於平臺的,這也超出了本書(同時也是標準C++庫自己)的範圍。
固然,在考慮使用平臺相關的工具以前,明白標準庫可以提供什麼是很重要的,那麼讓咱們經過一個例子來開始。
1.4 開始入門
好,如今你有一個很棒的與C++11兼容的編譯器。接下來呢?一個多線程C++程序是什麼樣子的?它看上去和其餘全部C++程序同樣,一般是變量、類以及函數的組合。惟一真正的區別在於某些函數能夠併發運行,因此你須要確保共享數據的併發訪問是安全的,詳見第3章。固然,爲了併發地運行函數,必須使用特定的函數以及對象來管理各個線程。
1.4.1 你好,併發世界
讓咱們從一個經典的例子開始:一個打印「Hello World.」的程序。一個很是簡單的在單線程中運行的Hello, World程序以下所示,當咱們談到多線程時,它能夠做爲一個基準。
1 #include <iostream> 2 int main() 3 { 4 std::cout << "Hello World\n"; 5 }
這個程序所作的一切就是將「Hello World」寫進標準輸出流。讓咱們將它與下面清單所示的簡單的Hello, Concurrent World程序作個比較,它啓動了一個獨立的線程來顯示這個信息。
清單 1.1一個簡單的Hello, Concurrent World程序
1 #include <iostream> 2 #include <thread> //① 3 void hello() //② 4 { 5 std::cout << "Hello Concurrent World\n"; 6 } 7 int main() 8 { 9 std::thread t(hello); //③ 10 t.join(); //④ 11 }
第一個區別是增長了#include<thread>①。在標準C++庫中對多線程支持的聲明在新的頭文件中:用於管理線程的函數和類在<thread>中聲明,而那些保護共享數據的函數和類在其餘頭文件中聲明。
其次,寫信息的代碼被移動到了一個獨立的函數中②。這是由於每一個線程都必須具備一個初始函數(initial function),新線程的執行在這裏開始。對於應用程序來講,初始線程是main(),可是對於全部其餘線程,這在std::thread對象的構造函數中指定──在本例中,被命名爲t③的std::thread對象擁有新函數hello()做爲其初始函數。
下一個區別:與直接寫入標準輸出或是從main()調用hello()不一樣,該程序啓動了一個全新的線程來實現,將線程數量一分爲二──初始線程始於main()而新線程始於hello()。
在新的線程啓動以後③,初始線程繼續執行。若是它不等待新線程結束,它就將自顧自地繼續運行到main()的結束,從而結束程序──有可能發生在新線程有機會運行以前。這就是爲何在④這裏調用join()的緣由──詳見第2章,這會致使調用線程(在main()中)等待與std::thread對象相關聯的線程,即這個例子中的t。
若是這看起來像是僅僅爲了將一條信息寫入標準輸出而作了大量的工做,那麼它確實如此──正如上文1.2.3節所描述的,通常來講並不值得爲了如此簡單的任務而使用多線程,尤爲是若是在這期間初始線程無所事事。在本書後面的內容中,咱們將經過實例來展現在哪些情景下使用多線程能夠得到明確的收益。
1.5 小結
在本章中,我說起了併發與多線程的含義以及在你的應用程序中爲何你會選擇使用(或不使用)它。我還說起了多線程在C++中的發展歷程,從1998標準中徹底缺少支持,經歷了各類平臺相關的擴展,再到新的C++11標準中具備合適的多線程支持。該支持到來的正是時候,它使得程序員們能夠利用隨着新的CPU而帶來的更增強大的硬件併發,由於芯片製造商選擇了以多核心的形式使得更多任務能夠同時執行的方式來增長處理能力,而不是增長單個核心的執行速度。
我在1.4節中的示例,來展現了C++標準庫中的類和函數有多麼的簡單。在C++中,使用多線程自己並不複雜,複雜的是如何設計代碼以實現其預期的行爲。
在嘗試了1.4節的示例以後,是時候看看更多實質性的內容了。在第2章中,咱們將看一看用於管理線程的類和函數。
[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.
原創文章,轉載請註明: 轉載自併發編程網 – ifeve.com
本文連接地址: 《C++ 併發編程》- 第1章 你好,C++的併發世界