8.2.1 作業系統和多執行緒
要在應用程式中實現多執行緒,必須有作業系統的支援。Linux 32位或64位作業系統對應用程式提供了多執行緒的支援,所以Windows NT/2000/XP/7/8/10是多執行緒作業系統。根據程序與執行緒的支援情況,可以把作業系統大致分為如下幾類:
(1)單程序、單執行緒,MS-DOS大致是這種作業系統。
(2)多程序、單執行緒,多數UNIX(及類UNIX的Linux)是這種作業系統。
(3)多程序、多執行緒,Win32(Windows NT/2000/XP/7/8/10等)、Solaris 2.x和OS/2都是這種作業系統。
(4)單程序、多執行緒,VxWorks是這種作業系統。
具體到Linux C++的開發環境,它提供了一套POSIX API函式來管理執行緒,使用者既可以直接使用這些POSIX API函式,也可以使用C++自帶的執行緒類。作為一名Linux C++開發者,這兩者都應該會使用,因為在Linux C++程式中,這兩種方式都有可能出現。
8.2.2 執行緒的基本概念
現代作業系統大多支援多執行緒概念,每個程序中至少有一個執行緒,所以即使沒有使用多執行緒程式設計技術,程序也含有一個主執行緒,所以也可以說,CPU中執行的是執行緒,執行緒是程式的最小執行單位,是作業系統分配CPU時間的最小實體。一個程序的執行說到底是從主執行緒開始的,如果需要,可以在程式任何地方開闢新的執行緒,其他執行緒都是由主執行緒建立的。一個程序正在執行,也可以說是一個程序中的某個執行緒正在執行。一個程序的所有執行緒共享該程序的公共資源,比如虛擬地址空間、全域性變數等。每個執行緒也可以擁有自己私有的資源,如堆疊、在堆疊中定義的靜態變數和動態變數、CPU暫存器的狀態等。
執行緒總是在某個程序環境中建立的,並且會在這個程序內部銷燬。執行緒和程序的關係是:執行緒是屬於程序的,執行緒執行在程序空間內,同一程序所產生的執行緒共享同一記憶體空間,當程序退出時,該程序所產生的執行緒都會被強制退出並清除。執行緒可與屬於同一程序的其他執行緒共享程序所擁有的全部資源,但是其本身基本上不擁有系統資源,只擁有一點在執行中必不可少的資訊(如程式計數器、一組暫存器和執行緒棧,執行緒棧用於維護執行緒在執行程式碼時需要的所有函式引數和區域性變數)。
相對於程序來說,執行緒所佔用的資源更少。比如建立程序,系統要為它分配很大的私有空間,佔用的資源較多;而對於多執行緒程式來說,由於多個執行緒共享一個程序地址空間,因此佔用的資源較少。此外,程序間切換時,需要交換整個地址空間,而執行緒間切換時,只是切換執行緒的上下文環境,因此效率更高。在作業系統中引入執行緒帶來的主要好處是:
(1)在程序內建立、終止執行緒比建立、終止程序要快。
(2)同一程序內執行緒間的切換比程序間的切換要快,尤其是使用者級執行緒間的切換。
(3)每個程序具有獨立的地址空間,而該程序內的所有執行緒共享該地址空間,因此執行緒的出現可以解決父子程序模型中子程序必須複製父程序地址空間的問題。
(4)執行緒對解決客戶/伺服器模型非常有效。
雖然多執行緒給應用開發帶來了不少好處,但並不是所有情況下都要去使用多執行緒,要具體問題具體分析。通常在下列情況下可以考慮使用多執行緒:
(1)應用程式中的各任務相對獨立。
(2)某些任務耗時較多。
(3)各任務有不同的優先順序。
(4)一些實時系統應用。
值得注意的是,一個程序中的所有執行緒共享它們父程序的變數,但同時每個執行緒可以擁有自己的變數。
8.2.3 執行緒的狀態
一個執行緒在從建立到結束這一生命週期中,總是處於下面4個狀態中的一個。
1)就緒態
執行緒能夠執行的條件已經滿足,只是在等待處理器(處理器要根據排程策略來把就緒態的執行緒排程到處理器中執行)。處於就緒態的原因可能是執行緒剛剛被建立(剛建立的執行緒不一定馬上執行,一般先處於就緒態),也可能是剛剛從阻塞狀態中恢復,還可能是因被其他執行緒搶佔而處於就緒態。
2)執行態
執行態表示執行緒正在處理器中執行,正佔用著處理器。
3)阻塞態
由於在等待處理器之外的其他條件而無法執行的狀態叫作阻塞態。這裡的其他條件包括I/O操作、互斥鎖的釋放、條件變數的改變等。
4)終止態
終止態就是執行緒的執行緒函式執行結束或被其他執行緒取消後處於的狀態。處於終止態的執行緒雖然已經結束了,但它所佔資源還沒有被回收,而且還可以被重新復活。我們不應該長時間讓執行緒處於這種狀態,執行緒處於終止態後應該及時進行資源回收,下面會講到如何回收。
8.2.4 執行緒函式
執行緒函式就是執行緒建立後進入執行態後要執行的函式。執行執行緒說到底就是執行執行緒函式。這個函式是我們自定義的,然後在建立執行緒時把我們的函式作為引數傳入執行緒建立函式。
同理,中斷執行緒的執行就是中斷執行緒函式的執行,以後再恢復執行緒的時候,就會在前面執行緒函式暫停的地方繼續執行下面的程式碼。結束執行緒也就不再執行執行緒函式。執行緒的函式可以是一個全域性函式或類的靜態函式,比如在POSIX執行緒庫中,它通常這樣宣告:
void *ThreadProc (void *arg);
其中,引數arg指向要傳給執行緒的資料,這個引數是在建立執行緒的時候作為引數傳入執行緒建立函式中的。函式的返回值應該表示執行緒函式執行的結果:成功還是失敗。注意函式名ThreadProc可以是自定義的函式名,這個函式是使用者自己先定義好,然後由系統來呼叫。
8.2.5 執行緒標識
既然控制代碼是用來標識執行緒物件的,那執行緒本身用什麼來標識呢?在建立執行緒的時候,系統會為執行緒分配唯一的ID作為執行緒的標識,這個ID從執行緒建立開始就存在,一直伴隨著執行緒的結束才消失。執行緒結束後,該ID就自動不存在,我們不需要去顯式清除它。
通常執行緒建立成功後會返回一個執行緒ID。
8.2.6 C++多執行緒開發的兩種方式
在Linux C++開發環境中,通常有兩種方式來開發多執行緒程式:一種是利用POSIX多執行緒API函式來開發多執行緒程式,另一種是利用C++自帶執行緒類來開發多執行緒程式。這兩種方式各有利弊。前一種方法比較傳統,後一種方法比較新,是C++11推出的方法。為何C++程式設計師也要熟悉POSIX多執行緒開發呢?這是因為C++11以前,在C++裡面使用多執行緒一般都是利用POSIX多執行緒API,或者把POSIX多執行緒API封裝成類,再在公司內部供大家使用。因此,一些老專案都是和POSIX多執行緒庫相關的,這也使得我們必須熟悉它,因為很可能進入公司後會要求維護以前的程式程式碼。而C++自帶執行緒類很可能在以後開發新的專案時會用到。總之,技多不壓身。
本文節選自《Linux C與C++一線開發實踐(第2版)》,獲出版社和作者授權釋出。