1. 程式人生 > >關於執行緒安全與可重入函式

關於執行緒安全與可重入函式

一、執行緒安全

一個函式被稱為執行緒安全的,當且僅當被多個併發執行緒反覆地呼叫時,它會一直產生正確的結果。如果一個函式不是執行緒安全的,我們就說它是執行緒不安全的。

四個(不相交的)執行緒不安全函式類:

1.不保護共享變數的函式

將這類執行緒不安全的函式變成執行緒安全的,相對比較容易:利用像P和V操作這樣的同步操作來保護共享的變數。這個方法的優點是在呼叫程式中不需要做任何的修改;缺點是同步操作將減慢程式的執行時間。

2.保持跨越多個呼叫的狀態的函式

一個偽隨機數生成器是這類執行緒不安全函式的簡單例子。rand函式是執行緒不安全的,因為當前呼叫的結果依賴於前次呼叫的中間結果,當呼叫srand為rand設定了一個種子後,我們從一個單執行緒中反覆地呼叫rand,能夠預期得到一個可重複的隨機數字序列。然而,如果多執行緒呼叫rand函式,這個假設就不再成立了。

使得像rand這樣的函式執行緒安全的唯一方法是重寫它,使得它不再使用任何static資料,而是依靠呼叫者在引數中傳遞狀態資訊。這樣做的缺點是程式設計師還要被迫修改呼叫程式中的程式碼。在一個大的程式中,可能有成百上千個不同的呼叫位置,做這樣的修改將是非常麻煩的,而且容易出錯。

3.返回指向靜態變數的指標的函式

某些函式,例如ctime和gethostbyname,將計算結果放在一個static變數裡,然後返回一個指向這個變數的指標。如果從併發執行緒中呼叫這些函式,那麼將可能發生災難,因為正在被一個執行緒使用的結果會被另一個執行緒悄悄地覆蓋了。

有兩種方法來處理這類執行緒不安全函式。一種選擇是重寫函式,使得呼叫者傳遞存放結果的變數的地址。這就消除了所有共享資料,但是它要求程式設計師能夠修改函式的原始碼。

如果執行緒不安全函式是難以修改或不可能修改的(例如程式碼非常複雜或是沒有原始碼可用),那麼另外一種選擇就是使用加鎖-拷貝(lock-and-copy)技術。基本思想是將執行緒不安全函式與互斥鎖聯絡起來。在每一個呼叫位置,對互斥鎖加鎖,呼叫執行緒不安全函式,將函式返回的結果拷貝到一個私有的儲存器位置,然後對互斥鎖解鎖。

4.呼叫執行緒不安全函式的函式

如果函式f呼叫執行緒不安全函式g,那麼f就是執行緒不安全的嗎?不一定,如果g是第2類函式,即依賴於跨越多次呼叫的狀態,那麼f也是執行緒不安全的,而且除了重寫g以外,沒有什麼辦法。然而,如果g是第1類或者第3類函式,那麼只要用一個互斥鎖保護呼叫位置和任何得到的共享資料,f仍然可能是執行緒安全的。

二、可重入性

可重入函式:是一類重要的執行緒安全函式,其特點在於它們具有這樣一種屬性,當它們被多個執行緒呼叫時,不會引入任何共享資料。

儘管執行緒安全與可重入有時會被用作同義詞,但它們並不完全等價。

可重入函式集合是執行緒安全函式的一個真子集。


可重入函式通常要比不可重入的執行緒安全的函式高效一些,因為它們不需要同步操作。將第2類執行緒不安全函式轉化為執行緒安全函式的唯一方法就是重寫它,使之變為可重入的。

例:rand函式的可重入版本(關鍵思想是用一個呼叫者傳遞進來的指標取代了靜態變數):

#include <stdio.h>
#include <unistd.h>

int rand_r(unsigned int *nextp)
{
	*nextp = *nextp * 1103515245 + 12345;
	return (unsigned int)(*nextp / 65536) % 32768;
}

int main()
{
	unsigned int i = 11;
	unsigned int *nextp = &i;
	while(1)
	{
		sleep(1);
		unsigned int ret = rand_r(nextp);
		printf("%ud\n", ret);
	}

	return 0;
}

如果所有的函式引數都是傳值傳遞的(即沒有指標),並且所有的資料引用都是本地的自動棧變數(即沒有引用靜態或全域性變數),那麼函式就是顯示可重入的。也就是說無論它是如何被呼叫的,我們都可以斷言它是可重入的。

然而,如果允許顯示可重入函式的一些引數是引用傳遞的(即允許它們傳遞指標),那麼我們就得到了一個隱式可重入的函式,也就是說,如果呼叫執行緒小心地傳遞指向非共享資料的指標,那麼它是可重入的。

大多數Unix函式,包括定義在標準C庫中的函式(例如malloc,free,printf,scanf)都是執行緒安全的,只有一小部分例外:


如果我們需要在一個執行緒化的程式中呼叫這些函式,對呼叫者來說最不惹麻煩的方法是加鎖-拷貝。然而,加鎖-拷貝方法有許多缺點。首先,額外的同步降低了程式的速度;其次,像gethostbyname這樣的函式返回指向複雜結構的指標,要拷貝整個結構層次,需要深層拷貝結構。且加鎖-拷貝這樣的方式對第2類執行緒不安全函式並不有效。

因此,Unix系統提供大多數執行緒不安全函式的可重入版本,儘可能地應該使用這些函式。