Linux中線程安全問題

一、線程安全的介紹

     在目前的計算機科學中,線程是操作系統調度的最小單元,進程是資源分配的最小單元。在大多數操作系統中,一個進程可以同時派生出多個線程。這些線程獨立執行,共享進程的資源。在單處理器系統中,多線程通過分時複用技術,處理器在不同的線程間切換,從而更高效地利用系統 CPU資源。在多處理器和多核系統中,線程實際上可以同時運行,也就是每個處理器可以運行一個線程,系統的運算能力相對於單線程或者單進程大幅增強。

   多線程技術在多個處理器機器,多核機器和集羣系統運行更快。因爲多線程模型與生俱來的優勢可以使這些機器或者系統實現真實地的併發執行。但多線程在帶來便利的同時,也引入一些問題。線程主要由控制流程和資源使用兩部分構成,因此一個不得不面對的問題就是對共享資源的訪問。爲了確保資源得到正確的使用,開發人員在設計編寫程序時需要考慮避免競爭條件和死鎖,需要更多地考慮使用線程互斥變量

線程安全 (Thread-safe) 的函數就是一個在代碼層面解決上述問題比較好的方法,也成爲多線程編程中的一個關鍵技術。如果在多線程併發執行的情況下,一個函數可以安全地被多個線程併發調用,可以說這個函數是線程安全的。反之,則稱之爲“非線程安全”函數。注意:在單線程環境下,沒有“線程安全”和“非線程安全”的概念。因此,一個線程安全的函數允許任意地被任意的線程調用,程序開發人員可以把主要的精力在自己的程序邏輯上,在調用時不需要考慮鎖和資源訪問控制,這在很大程度上會降低軟件的死鎖故障和資源併發訪問衝突的機率。所以,開發人員應儘可能編寫和調用線程安全函數。

二、如何編寫線程安全函數

判斷一個函數是否線程安全不是一件很容易的事情。但是讀者可以通過下面這幾條確定一個函數是線程不安全的。

  • a, 函數中訪問全局變量和堆。
  • b, 函數中分配,重新分配釋放全局資源。
  • c, 函數中通過句柄和指針的不直接訪問。
  • d, 函數中使用了其他線程不安全的函數或者變量。

因此在編寫線程安全函數時,要注意兩點:

  • 1, 減少對臨界資源的依賴,儘量避免訪問全局變量,靜態變量或其它共享資源,如果必須要使用共享資源,所有使用到的地方必須要進行互斥鎖 (Mutex) 保護;
  • 2, 線程安全的函數所調用到的函數也應該是線程安全的,如果所調用的函數不是線程安全的,那麼這些函數也必須被互斥鎖 (Mutex) 保護;

舉個例子(參考 例子 1),下面的這個函數 sum()是線程安全的,因爲函數不依賴任何全局變量。

例子1

int sum(int i, int j) { 
     return (i+j); 
}

但如果按下面的方法修改,sum()就不再是線程安全的,因爲它調用的函數 inc_sum_counter()不是線程安全的,該函數訪問了未加鎖保護的全局變量 sum_invoke_counter。這樣的代碼在單線程環境下不會有任何問題,但如果調用者是在多線程環境中,因爲 sum()有可能被併發調用,所以全局變量 sum_invoke_counter很有可能被併發修改,從而導致計數出錯。

例子 2

static int sum_invoke_counter = 0; 
 
void inc_sum_counter(int i, int j) { 
    sum_invoke_counter++; 
} 
   
int sum(int i, int j) { 
   inc_sum_counter(); 
   return (i+j); 
}

我們可通過對全局變量 sum_invoke_counter添加鎖保護,使得 inc_sum_counter()成爲一個線程安全的函數。

例子 3

static int sum_invoke_counter = 0; 
static pthread_mutex_t sum_invoke_counter_lock = PTHREAD_MUTEX_INITIALIZER; 
 
void inc_sum_counter(int i, int j) { 
   pthread_mutex_lock( &sum_invoke_counter_lock ); 
   sum_invoke_counter++; 
   pthread_mutex_unlock( &sum_invoke_counter_lock ); 
} 
   
int sum(int i, int j) { 
   inc_sum_counter(); 
   return (i+j); 
}

現在 , sum()和 inc_sum_counter()都成爲了線程安全函數。在多線程環境下,sum()可以被併發的調用,但所有訪問 inc_sum_counter()線程都會在互斥鎖 sum_invoke_counter_lock上排隊,任何一個時刻都只允許一個線程修改 sum_invoke_counter,所以 inc_sum_counter()就是現成安全的。

三、可重入

  除了線程安全還有一個很重要的概念就是 可重入(Re-entrant),所謂可重入,即:當一個函數在被一個線程調用時,可以允許被其他線程再調用。顯而易見,如果一個函數是可重入的,那麼它肯定是線程安全的。但反之未然,一個函數是線程安全的,卻未必是可重入的。程序開發人員應該儘量編寫可重入的函數。

一個函數想要成爲可重入的函數,必須滿足下列要求:

  • a) 不能使用靜態或者全局的非常量數據
  • b) 不能夠返回地址給靜態或者全局的非常量數據
  • c) 函數使用的數據由調用者提供
  • d) 不能夠依賴於單一資源的鎖
  • e) 不能夠調用非可重入的函數

對比前面的要求,例子 1的 sum()函數是可重入的,因此也是線程安全的。例子 3中的 inc_sum_counter()函數雖然是線程安全的,但是由於使用了靜態變量和鎖,所以它是不可重入的。因爲 例子 3中的 sum()使用了不可重入函數 inc_sum_counter(), 它也是不可重入的。

四、實現線程同步的方法:

信號量、互斥鎖、讀寫鎖、條件變量

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章