使用面向對象組件構建多線程應用程序時,程序員必須考慮的重要兩點是:
a. 構建應用程序需要的組件;
b. 這些組件在多線程環境中的行為。
1. 線程、對象和作用域
對象具有如下4種作用域類型:
a. 局部作用域(local scope); b. 函數作用域(function scope); c. 文件作用域(file scope); d. 類作用域(class scope)。
1.1 連接與作用域
一個線程可以訪問另一個線程的堆棧片斷。當一個程序創建多個線程時,每個線程都可以訪問創建它們的進程的數據片斷、堆棧片斷、代碼片斷以及堆。當多個線程訪問一個對象時,作用域、對象以及對象的連接決定了線程是否可以直接訪問這個對象;或者這個對象是否必須通過參數從一個線程傳遞到另一個線程。
當文件作用域對象具有外部連接時,它可以被創建該對象的進程中的任何線程自由訪問,不需要傳遞參數。文件作用域對象和外部連接對于整個進程及其所有線程是全局性的。另一方面,文件作用域對象和內部連接可以被同一翻譯單元的函數部分自由訪問,不必進行參數傳遞。不過,為了被其它翻譯單元中的函數訪問,該對象必須作為一個參數進行傳遞。就像程序可以執行在多個翻譯單元中定義的函數和過程一樣,線程也可以執行在多個翻譯單元中定義的函數和過程。
如果線程在執行一個函數,這個函數就容易訪問位于函數局部翻譯單元內的對象以及具有文件作用域和外部連接的對象。為了讓線程的函數訪問具有文件作用域和內部連接的對象,必須通過參數將對象傳遞給線程的函數。
至少有一種方法可以從進程內的任一個線程訪問任一個對象。這種訪問可以是單向的,即表示接收線程只能讀對象,而不能更改它。這就是通過值傳遞對象的情況,這與通過引用傳遞相反。同樣,如果將對象聲明為const,情況也是這樣。如果對象通過引用或指針來傳遞,則對象充當線程間的通信鏈接,而且線程間的通信是雙向通信。這意味著線程A對象所作的任何個性都立即影響線程B,同時線程B所做的修改也立即影響到線程A。
1.2 線程和類作用域
設想在一個對象的成員函數內創建了一個線程,此時成員函數和線程并沒有父-子關系。由成員函數傳遞的線程在訪問對象的內部數據成員上沒有任何特殊優先權。一旦創建了線程,對象訪問就由對象作用域和連接所控制。
2. 同步關系和對象成員函數
在一個進程內,對象與多個線程可能具有4種基本同步關系:
a. 對象被需要同步的多個線程訪問; b. 對象被分解成需要同步的多個線程; c. 對象被分解成需要同步的多個線程,同時,對象也能被需要同步的多個線程訪問; d. 對象只能被單個線程訪問,而且不能分解成多個線程。
當對象創建的線程試圖并發修改對象的臨界區時發生通信依賴性關系。為了防止線程破壞對象的臨界區,這些線程間通過互斥量來通信。當多個線程為共同的目標而工作,每個線程解決問題的一部分,此時就發生同步依賴性關系。每個部分的執行必須按正確的序列進行,以便對象從整體上協調完成它的工作。同步化對象的線程所執行的任務,線程將使用條件變量、等待函數或事件互斥量。
3. 在多線程環境中構建和析構對象
3.1 exit()和abort()
當使用exit()時,系統嘗試清空緩沖器、釋放資源,并在可能的地方調用析構函數。如果析構函數中存在鎖定或釋放機制,則調用exit()通常會讓掛起的析構函數執行。
當使用abort()調用時,所有執行都被取消。如果存在還沒有調用的析構函數,執行abort()請求后它們不被調用。如果這些析構函數包含一些當前占有互斥量的取消鎖定或釋放機制,則應用程序將退出并保持對互斥量的占有權,其它所有等待該互斥量的進程或線程可能被無限延遲。在多線程環境中強烈推薦使用C++異常處理機制,在程序失敗或拋出異常時,結構化異常處理讓程序員有機會決定通過同步機制如何應付。
3.2 構造函數和SS關系
在同一個進程中創建多個線程時,它們可能并發執行、同時執行,也可能異步執行。
兩個線程并發執行時,它們在同一時間段內執行,不一定在同一時刻執行;
多個線程同時執行時,則是指它們在同一時刻執行。只有多處理器系統可以讓某進程中的多個線程在同一時刻執行;
多個線程異步執行時,它們可能并發執行、同時執行,也可能會依次執行。對于異步執行,不能保證多個線程的執行方式。在異步執行的進程中,線程執行的順序得不到保證。
3.3 析構函數與FF關系
3.4 線程集合與對象
在使用接口類封裝操作系統API時,重要的優勢是封裝(encapsulation)和保護(protection)。
在多線程環境中,保護互斥量或條件變量不漂浮不定是消除導致競爭條件、死鎖以及無限延遲情況發生所必需的。我們還可以使用封裝來控制線程取消,即通過另一個線程的調用終止一個線程的執行。
取消線程可以有效停止它的跟蹤和銷毀。
由于某些原因,一個線程被鎖定,而且不能對任何通信嘗試作出反應,這樣的線程必須取消。陷入致命或無限循環的線程必須取消。不過,在大部分情況下,應當使用條件變量和事件互斥量來控制其活動不再需要的線程。防止雜亂線程取消的第一步是在某個對象中封裝線程句柄。它只能被類的成員函數訪問。唯一取消這個對象占有的線程的途徑是通過cancel成員函數。如果線程對象被它的宿主對象私有性地占有,則只有宿主對象才能取消此線程。而如果線程的句柄為全局變量,或者可以被多個線程訪問,它可能被錯誤地取消。隨著在應用程序中添加更多的模塊和線程,這種犯錯的可能性也增加。
3.5 線程與異常處理
異常處理所隱含的基本思想是讓不能處理特定問題的組件將該問題傳遞給另一個知道如何解決此問題的組件。使用異常處理,我們可以設計特殊目的的組件,它的特定功能就是應付錯誤(在發生錯誤的情況下),并且處理其它軟件異常。
C++異常處理機制提供的三種重要功能:
a. 允許沒有返回值的函數拋出異常對象;
b. 允許函數將拋出結構用作替換返回機制來返回多種類型返回值;
c. 允許程序員以異常對象的形式定義情景敏感性診斷(situation sensitive diagnostics)。這些異常對象可以讓異常情景更容易理解、維護和調試。
異常的拋出和捕獲是通過調用堆棧的協助來完成的。進程中的每個線程都有自己的調用堆棧,因此不要試圖從一個線程到另一個線程拋出異常。
可以借助于異常處理機制來識別和處理死鎖。拋出異常的進程可以包含標識死鎖情形的邏輯。拋出對象可以包含針對每個侵犯線程的線程對象。處理器然后決定做什么。處理器可以強迫從兩個線程移除資源(如果可能的話)。處理器可以決定取消兩個線程,從整體上保持系統的執行。處理器處理完死鎖后,處理器可以執行一些可選路徑,這是為碰到死鎖后預備的。使用異常機制和用戶自定義,異常類可以提高多線程應用程序順利執行而不崩潰的成功率。
4. 線程安全函數
當函數在某一時刻被多個線程調用,而且不要求任何施加于調用者部分的動作時,函數或函數集被認為是線程安全的或可以重復進入的。對于某些或包含靜態變量、訪問全局數據,或不能重復進入的函數就是不安全的。當前大部分編譯器提供了標準庫的多線程版本。在可能應用于多線程環境的時候,程序員應當連接標準標準庫的多線程版本。
5. 多線程環境中的不安全函數
如果不知道來自庫的哪一個函數是安全的,哪一個是不安全的,程序員就有3種選擇:
a. 不要在應用程序中使用任何不安全函數;
b. 限制在單線程中使用所有不安全函數;
c. 通過一套同步機制封裝所有的可能不安全函數。
6. 在多線程架構中使用STL算法
為了使用STL我們可選擇犧牲靈活性換取線程安全。我們使用接口類來解決問題,通過復合結合容器類與接口類,而且使用成員函數封裝STL算法來達到線程安全的標準。