線程安全

線程安全程式設計中的術語,指某個函數函式庫多線程環境中被呼叫時,能夠正確地處理多個線程之間的公用變數,使程式功能正確完成。

假設有間銀行只有 1000 元,而兩個人同時提領 1000 元時就可能會拿到總計 2000 元的金額。為了避免這個問題,該間銀行提款時應該使用互斥鎖,即意味着針對同一個資源處理時,前一個人提領交易完成後才處理下一筆交易。但這種手法會使得效能降低。

一般來說,線程安全的函數應該為每個呼叫它的線程分配專門的空間,來儲存需要單獨儲存的狀態(如果需要的話),不依賴於「線程慣性」,把多個線程共用的變數正確對待(如,通知編譯器該變數為「易失(volatile)」型,阻止其進行一些不恰當的最佳化),而且,線程安全的函數一般不應該修改全域對象。

很多C庫代碼(比如某些strtok的實現,它將「多次呼叫中需要保持不變的狀態」儲存在靜態變數中,導致不恰當的共用)不是線程安全的,在多線程環境中呼叫這些函數時,要進行特別的預防措施,或者尋找別的替代方案。

例子

在下面這段代碼中,函數increment_counter是線程安全的,但不是可重入的。

#include <pthread.h>

int increment_counter ()
{
	static int counter = 0;
	static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

	pthread_mutex_lock(&mutex);
	
	// only allow one thread to increment at a time
	++counter;
	// store value before any other threads increment it further
	int result = counter;	

	pthread_mutex_unlock(&mutex);
	
	return result;
}

上面的代碼中,函數increment_counter可以在多個線程中被呼叫,因為有一個互斥鎖mutex來同步對共用變數counter的訪問。但是如果這個函數用在可重入的中斷處理程式中,如果在pthread_mutex_lock(&mutex)和pthread_mutex_unlock(&mutex)之間產生另一個呼叫函數increment_counter的中斷,則會第二次執行此函數,此時由於mutex已被lock,函數會在pthread_mutex_lock(&mutex)處阻塞,並且由於mutex沒有機會被unlock,阻塞會永遠持續下去。簡言之,問題在於 pthread 的 mutex 不可重入。

解決辦法是設定 PTHREAD_MUTEX_RECURSIVE 屬性。然而對於給出的問題而言,專門使用一個 mutex 來保護一次簡單的增量操作顯然過於昂貴,因此c++11中的原子變數提供了一個可使此函數既線程安全又可重入(而且還更簡潔)的替代方案:

#include <atomic>

int increment_counter ()
{
	static std::atomic<int> counter(0);
	
	// increment is guaranteed to be done atomically
	int result = ++counter;

	return result;
}

外部連結