通過多線程為C++并發(fā)提供標準化支持是件新鮮事。只有在C++11標準下,才能編寫不依賴平臺擴展的多線程代碼。了解C++線程庫中的眾多規(guī)則前,先來了解一下其發(fā)展的歷史。
C++98(1998)標準不承認線程的存在,并且各種語言要素的操作效果都以順序抽象機的形式編寫。不僅如此,內存模型也沒有正式定義,所以在C++98標準下,沒辦法在缺少編譯器相關擴展的情況下編寫多線程應用程序。
當然,編譯器供應商可以自由地向語言添加擴展,添加C語言中流行的多線程API———POSIX標準中的C標準和Microsoft Windows API中的那些———這就使得很多C++編譯器供應商通過各種平臺相關擴展來支持多線程。這種編譯器支持一般受限于只能使用平臺相關的C語言API,并且該C++運行庫(例如,異常處理機制的代碼)能在多線程情況下正常工作。因為編譯器和處理器的實際表現(xiàn)很不錯了,所以在少數(shù)編譯器供應商提供正式的多線程感知內存模型之前,程序員們已經(jīng)編寫了大量的C++多線程程序了。
由于不滿足于使用平臺相關的C語言API來處理多線程,C++程序員們希望使用的類庫能提供面向對象的多線程工具。像MFC這樣的應用框架,如同Boost和ACE這樣的已積累了多組類的通用C++類庫,這些類封裝了底層的平臺相關API,并提供用來簡化任務的高級多線程工具。各種類和庫在細節(jié)方面差異很大,但在啟動新線程的方面,總體構造卻大同小異。一個為許多C++類和庫共有的設計,同時也是為程序員提供很大便利的設計,也就是使用帶鎖的獲取資源即初始化(RAII, Resource Acquisition Is Initialization)的習慣,來確保當退出相關作用域時互斥元解鎖。
編寫多線程代碼需要堅實的編程基礎,當前的很多C++編譯器為多線程編程者提供了對應(平臺相關)的API;當然,還有一些與平臺無關的C++類庫(例如:Boost和ACE)。正因為如此,程序員們可以通過這些API來實現(xiàn)多線程應用。不過,由于缺乏統(tǒng)一標準的支持,缺少統(tǒng)一的線程內存模型,進而導致一些問題,這些問題在跨硬件或跨平臺相關的多線程應用上表現(xiàn)得尤為明顯。
所有的這些隨著C++11標準的發(fā)布而改變了,新標準中不僅有了一個全新的線程感知內存模型,C++標準庫也擴展了:包含了用于管理線程(參見第2章)、保護共享數(shù)據(jù)(參見第3章)、線程間同步操作(參見第4章),以及低級原子操作(參見第5章)的各種類。
新C++線程庫很大程度上,是基于上文提到的C++類庫的經(jīng)驗積累。特別是,Boost線程庫作為新類庫的主要模型,很多類與Boost庫中的相關類有著相同名稱和結構。隨著C++標準的進步,Boost線程庫也配合著C++標準在許多方面做出改變,因此之前使用Boost的用戶將會發(fā)現(xiàn)自己非常熟悉C++11的線程庫。
如本章起始提到的那樣,支持并發(fā)僅僅是C++標準的變化之一,此外還有很多對于編程語言自身的改善,就是為了讓程序員們的工作變得更加輕松。這些內容在本書的論述范圍之外,但是其中的一些變化對于線程庫本身及其使用方式產(chǎn)生了很大的影響。附錄A會對這些特性做一些介紹。
新的C++標準直接支持原子操作,允許程序員通過定義語義的方式編寫高效的代碼,從而無需了解與平臺相關的匯編指令。這對于試圖編寫高效、可移植代碼的程序員們來說是一個好消息;編譯器不僅可以搞定具體平臺,還可以編寫優(yōu)化器來解釋操作語義,從而讓程序整體得到更好的優(yōu)化。
通常情況下,這是高性能計算開發(fā)者對C++的擔憂之一。為了效率,C++類整合了一些底層工具。這樣就需要了解相關使用高級工具和使用低級工具的開銷差,這個開銷差就是抽象代價(abstraction penalty)。
C++標準委員會在設計標準庫時,特別是設計標準線程庫的時候,就已經(jīng)注意到了這點;目的就是在實現(xiàn)相同功能的前提下,直接使用底層API并不會帶來過多的性能收益。因此,該類庫在大部分主流平臺上都能實現(xiàn)高效(帶有非常低的抽象代價)。
C++標準委員會為了達到終極性能,需要確保C++能給那些要與硬件打交道的程序員,提供足夠多的的底層工具。為了這個目的,伴隨著新的內存模型,出現(xiàn)了一個綜合的原子操作庫,可用于直接控制單個位、字節(jié)、內部線程間同步,以及所有變化的可見性。原子類型和相應的操作現(xiàn)在可以在很多地方使用,而這些地方以前可能使用的是平臺相關的匯編代碼。使用了新標準的代碼會具有更好的可移植性,而且更容易維護。
C++標準庫也提供了更高級別的抽象和工具,使得編寫多線程代碼更加簡單,并且不易出錯。有時運用這些工具確實會帶來性能開銷,因為有額外的代碼必須執(zhí)行。但是,這種性能成本并不一定意味著更高的抽象代價;總體來看,這種性能開銷并不比手工編寫等效函數(shù)高,而且編譯器可能會很好地內聯(lián)大部分額外代碼。
某些情況下,高級工具會提供一些額外的功能。大部分情況下這都不是問題,因為你沒有為你不使用的那部分買單。在罕見的情況下,這些未使用的功能會影響其他代碼的性能。如果你很看重程序的性能,并且高級工具帶來的開銷過高,你最好是通過較低級別的工具來實現(xiàn)你需要的功能。絕大多數(shù)情況下,額外增加的復雜性和出錯幾率都遠大于性能的小幅提升帶來的收益。即便是有證據(jù)確實表明瓶頸出現(xiàn)在C++標準庫的工具中,也可能會歸咎于低劣的應用設計,而非低劣的類庫實現(xiàn)。例如,如果過多的線程競爭一個互斥單元,將會很明顯的影響性能。與其在互斥操作上耗費時間,不如重新設計應用,減少互斥元上的競爭來得劃算。如何減少應用中的競爭,會在第8章中再次提及。
在C++標準庫沒有提供所需的性能或行為時,就需要使用與平臺相關的工具。
雖然C++線程庫為多線程和并發(fā)處理提供了較全面的工具,但在某些平臺上提供額外的工具。為了方便地訪問那些工具的同時,又使用標準C++線程庫,在C++線程庫中提供一個native_handle()
成員函數(shù),允許通過使用平臺相關API直接操作底層實現(xiàn)。就其本質而言,任何使用native_handle()
執(zhí)行的操作都是完全依賴于平臺的,這超出了本書(同時也是標準C++庫本身)的范圍。
所以,使用平臺相關的工具之前,要明白標準庫能夠做什么,那么下面通過一個栗子來展示下吧。