執行緒安全性

執行緒安全性

當對一個複雜對象進行某種操作時,從操作開始到操作結束,被操作的對象往往會經歷若干非法的中間狀態。調用一個函式(假設該函式是正確的)操作某對象常常會使該對象暫時陷入不可用的狀態(通常稱為不穩定狀態),等到操作完全結束,該對象才會重新回到完全可用的狀態。如果其他執行緒企圖訪問一個處於不可用狀態的對象,該對象將不能正確回響從而產生無法預料的結果,如何避免這種情況發生是執行緒安全性的核心問題。

基本介紹

  • 中文名:執行緒安全性
  • 外科醫生做手術有點象
  • 調用:一個函式
  • 安全性:是存在多種級別的
簡單類比,執行緒安全性的級別,基本知識,例子說明,特殊情況,總結,基本理論,一般準則,

簡單類比

執行緒安全性問題跟外科醫生做手術有點象,儘管手術的目的是改善患者的健康,但醫生把手術過程分成了幾個步驟,每個步驟如果不是完全結束的話,都會嚴重損害患者的健康。想想看,如果一個醫生切開患者的胸腔後要休三周假會怎么樣?然而單執行緒的程式中是不存在這種問題的,因為在一個執行緒更新某對象的時候不會有其他執行緒也去操作同一個對象。(除非其中有異常,異常是可能導致上述問題的。當一個正在更新某對象的執行緒因異常而中斷更新過程後,再去訪問沒有完全更新的對象,會出現同樣的問題)

執行緒安全性的級別

基本知識

就執行緒安全性進行討論的時候存在這樣一個問題:執行緒的安全性是存在多種級別的,每個人談論的級別其實並不相同,僅僅說某段代碼不具備執行緒安全性並不能說明全部問題。然而許多人對執行緒的安全性有一些想當然的預期,有些時候這些預期是合理而合法的,但有些時候不是。下面給出一些此類的預期:
通常認為多個執行緒讀某對象時不會產生問題,問題只會在更新對象的時候出現,因為只有那時對象才會被修改, 從而有進入不穩定狀態的危險。然而,某些對象具有內部狀態,即使在讀的時候內部狀態也會被改變(比如某些對象有內部緩衝)。假如兩個執行緒去讀取這種對象,問題仍然會產生,除非該對象的讀操作設計已經採用了合適的多執行緒處理方法。

例子說明

通常認為更新兩個相互獨立的對象時,即使它們的類型相同也不會有問題。一般假設相互獨立的對象之間是互不相關的,一個對象的不穩定狀態並不會對另一個對象產生影響。然而,一些對象在內部是存在數據共享的(如靜態的類數據,全局數據等),這使它們即使看上去沒有什麼邏輯上的關係,內部卻依然存在聯繫。這種情況下,修改兩個“相互獨立”的對象怎么都會產生問題。考慮下面的情況:
void f( ) { std::string x; // Modify x. }
void g( ) { std::string y; // Modify y. }
如果一個執行緒運行函式f()修改x,另一個執行緒運行函式g()修改y,這種情況下會出現問題嗎?大部分人認為這是兩個明顯獨立的對象,可以被同時改動而不會導致任何問題。但是,有一種可能性就是,兩個對象內部存在共享數據,這完全依賴於std::string的實現,如果那樣的話,同時改動便是有問題的了。實際上,如果兩個對象內部存在共享數據的話,即使一個函式僅僅是讀取對象,問題依然存在,因為改動另一個對象的函式可能觸及內部的共享數據。
通常認為即便使用一個通用的資源池,獲取資源的函式也不存線上程安全性的問題。例如:
void f() { char *p = new char[512]; // use the array p}
void g() { char *p = new char[512]; // use the array p}
如果一個執行緒執行函式f(),另一個執行緒執行函式g(),兩個執行緒可能同時使用操作符new去分配記憶體。在多執行緒環境中,只有假設new操作符的實現已經考慮到這種情況,從同一個記憶體池中獲取記憶體也設計了正確的處理方法,才可以認為安全性得到保證。實際中,new操作符在內部確實會同步這些執行緒,因而每次調用都能夠得到獨立的記憶體分配而不損壞共享的記憶體池。與此類似的操作還有檔案的打開、網路連線的發起以及其他資源的分配。
人們一般認為會引起問題的情形是一個執行緒要訪問(讀或更新)某對象時另一個執行緒正在更新它。全局對象尤其易於出現這種問題,局部對象出現問題的情況則少的多。比如:
std::string x;
void f() { std::string y; // modify x and y. }
如果兩個執行緒同時進入函式f(),它們拿到的對象y是不相同的,這是由於不同的執行緒擁有各自不同的棧,局部變數都線上程自己的棧上分配,因而每個執行緒都會拿到自己獨立的局部變數副本。所以說,在f()中對y進行操作不會產生問題(假定操作獨立的對象有安全保證),然而,全局對象x僅有一份兩個執行緒都會觸及的拷貝,對x的如上操作便會產生問題。局部變數也不是完全不會產生問題,因為每個函式都能夠啟動新的執行緒並且把局部變數的指針作為該執行緒的一個輸入參數,比如:
void f ( ) { std : : string x ; startthread (somefunction , &x); startthread (somefunction , &x); }

特殊情況

這裡假設有一個名為startthread的庫函式,它有兩個參數,一個是執行緒入口函式的指針,一個是執行緒入口函式的參數的指針。在此情況下我們調用startthread啟動兩個執行緒,並且把x對象作為參數同時傳給兩個執行緒。如果somefunction()中會對x進行修改,則兩個執行緒可能修改同一個對象,類似的問題便產生了。注意,這種情況是特別隱蔽的,因為somefunction()並沒有什麼特別的理由去考慮兩次調用會傳給它同一個對象,因而它不大可能做出應付這種情況的保護措施。

總結

基本理論

如果一個函式在其文檔中沒有特別註明具備執行緒安全性,則應該認為它不具備。許多庫大量使用了內部的靜態數據,除非它是為多執行緒套用所設計,否則要牢記其內部數據可能沒有利用互斥量進行適當的保護。類似,如果類的成員函式在其文檔中沒有特別註明對於多執行緒套用是安全的話,則認為它不安全。兩個執行緒去操作相同的對象會引起問題,這是顯而易見的,然而,即使兩個執行緒去操作不同的物體依然會引起問題。出於多種原因,許多類使用了內部靜態數據或者在多個看上去明顯不同的對象間共享實現細則,

一般準則

以下給出幾個一般準則:
作業系統提供的API具備執行緒安全性
POSIX執行緒標準要求C標準庫中的大多數函式具備執行緒安全性,少數例外會在C標準中註明。
對於Windows提供的C標準庫,如果所使用的版本沒有問題,而且進行了正確的初始化,他們都是安全的。
C++標準庫的執行緒安全性不是很明確,它在很大程度上依賴於使用的編譯器標準模板庫執行緒安全性的SGI準則作為實際中的標準取得很大進展,但並不是統一的標準。

相關詞條

熱門詞條

聯絡我們