線程安全與可重入性


概述

一組並發線程運行在同一進程上下文中,每個線程都有自己獨立的線程上下文,包括線程ID、棧、棧指針、程序計數器(PC)、條件碼和通用目的寄存器值。每個線程和其它線程一起共享進程上下文的其他部分,包括整個用戶虛擬地址空間(由代碼段、讀/寫數據、堆以及所有共享庫的代碼和數據區組成)。線程也共享打開的文件集合。
當存在共享資源的時候,對資源的訪問需要同步。這時候使用線程編寫程序的時候,需要編寫具有線程安全性(thread safety)屬性的函數。一個函數,當且僅當被多個並發線程反復調用時,能夠一直產生正確的結果,才能夠被稱為線程安全的(thread-safe),否則我們稱其為非線程安全的(thread-unsafe)。

四類線程不安全函數

第1類;不保護共享變量的函數

對共享變量的並發訪問會造成競爭,請看下面的例子:

// badCounter.c

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include "PV.h"

void *thread(void *vargp);

volatile int Counter = 0;

int main(int argc, char **argv)
{
	int niters;
	pthread_t tid1, tid2;
	
	if(argc != 2)
	{
		fprintf(stderr, "Usage : %s <niters>\n", argv[0]);
		exit(0);
	}

	niters = atoi(argv[1]);
	
	pthread_create(&tid1, NULL, thread, &niters);
	pthread_create(&tid2, NULL, thread, &niters);
	pthread_join(tid1, NULL);
	pthread_join(tid2, NULL);
	
	if(Counter != 2 * niters)
		fprintf(stderr, "BOOM! Counter = %d\n", Counter);
	else
		fprintf(stdout, "OK! Counter = %d\n", Counter);
	
	exit(0);
}

void *thread(void *vargp)
{
	int i, niters = *((int*)vargp);
	
	for(i = 0; i < niters; ++i)
		Counter++;
		
	return NULL;
}

運行一下這個例子:

$ gcc -pthread badCounter.c -o badCounter

$ ./badCounter 100000000
BOOM! cnt = 177953661

$ ./badCounter 100000000
BOOM! cnt = 185993828

$ ./badCounter 100000000
BOOM! cnt = 176414058

如果我們每次運行這個例子,結果可能都不一致,原因是:對Counter的訪問存在競爭。
解決這類問題的方法是:利用像P和V操作這樣的同步操作來保護共享的變量。這樣做有好處也有壞處:

  • 優點是在調用程序中不需要做任何修改
  • 缺點是同步操作將增加程序的執行時間

第2類:依賴於跨越多個調用的狀態的函數

一個偽隨機數生成器設這類線程不安全函數的簡單例子:

unsigned int next = 1;

/* rand - 返回一個在[0, 32767]范圍內的偽隨機數*/
int rand()
{
	next = next * 1103515245 + 12345;
	return (unsigned int)(next / 65536) % 32768;
}

/* srand - 為rand函數設置隨機種子*/
void srand(unsigned int seed)
{
	next = seed;
}

rand函數是線程不安全的,因為調用當前調用的結果依賴於前次調用的中間結果。當調用srandrand設置一個種子后,我們從一個單線程中反復調用rand,能夠預期得到一個可重復的隨機數字序列。然而在多線程調用rand函數,這種假設就不再成立了。

解決這一類問題唯一的方法是重寫它,使得它不再使用任何static數據,而是依靠調用者在參數中傳遞狀態信息。這樣做的缺點很明顯:現在需要被迫修改調用rand的代碼。在一個大的程序中,可能有成百上千不同的調用位置,做這樣的修改將是非常麻煩的,而且容易出錯。

第3類:返回指向靜態變量的指針的函數

一些函數,如ctime和gethostbyname,將計算結果存放在一個static變量中,然后返回一個指向這種變量的指針。如果我們從並發線程中調用這些函數,那么將可能發生災難,因為正在被一個線程使用的結果會被另一個線程悄悄覆蓋了。
有兩種方法來處理這類線程不安全函數:

  • 第一種選擇是重寫函數,使得調用者傳遞存放結果的變量的地址,這就消除了所有共享數據,但是這要求程序員能夠修改函數的源代碼。
  • 第二種選擇是使用加鎖-拷貝(lock-and-copy)技術。如果線程不安全函數難以修改或者不能修改(如代碼過於復雜或者無法獲得其源代碼),可以采用這種方式。加鎖-拷貝的基本思想是將線程不安全函數與互斥鎖聯系起來。在每個調用位置,對互斥鎖加鎖,調用線程不安全函數 ,將函數返回的結果拷貝到一個私有的存儲器位置,然后對互斥鎖解鎖。為盡可能減少對調用者的修改,應該定義一個線程安全的包裝函數,它執行加鎖-拷貝,然后通過調用這個包裝函數來取代對線程不安全函數的調用。

下面利用加鎖-拷貝技術,給出ctime的一個線程安全的版本:

char *ctime_TS(const time_t *timep, char *privatep)
{
	char *sharedp;
	
	P(&mutex);		// 加鎖
	sharedp = ctime(timep);
	strcpy(privatep, sharedp);	// 拷貝到私有的存儲器空間
	V(&mutex);		// 解鎖
	return privatep;
} 

第4類:調用線程不安全函數的函數

如果函數f調用線程不安全函數g,那么f不一定是線程不安全的。

  • 如果g是第2類函數(依賴於跨越多個調用的狀態),那么f是線程不安全的函數。除了重寫g以外,沒有其他辦法。
  • 如果g是第1類或第3類函數,那么只要用一個互斥鎖保護調用位置和任何得到的共享數據,f仍然可能是線程安全的。上面的ctime和ctime_ts就是個很好的例子。

以下是一些線程不安全函數,以及它們的對應的線程安全版本。在編寫並發線程程序的時候,盡可能使用線程安全版本的函數。

可重入性

有一類重要的線程安全函數,叫做可重入函數(reentrant function),其特點在於它們具有這樣一種屬性:當它們被多個線程調用時,不會引用任何共享數據。所有函數的集合被划分成不相交的線程安全和線程不安全函數集合。可重入函數集合是線程安全函數集合的一個真子集。它們的關系如下圖所示。

可重入函數通常要比不可重入函數的線程安全的函數高效一些,因為它們不需要同步操作!更進一步說,線程不安全函數rand轉化為可重入函數,就不要引用外部的共享變量,我們可以用一個調用者傳遞進來的指針取代靜態的next變量:

/* rand_r 是一個可重入的偽隨機數生成函數*/
int rand_r(unsigned int *nextp)
{
	*nextp = *nextp * 1103515245 + 12345;
	return (unsigned int)(*next / 65536) % 32768;
}

上面的rand_r是可重入的嗎?不一定。上面說過了,可重入函數不引用任何共享變量,但是我們不能夠保證調用者傳遞進來的nextp不是指向一個共享變量(比如全局變量或靜態變量)!
實際上,我們所說的可重入函數包含兩類:顯式可重入函數(explicitly reentrant function)和隱式可重入函數(implicitly reentrant function)

顯式可重入函數

如果所有的函數參數都是傳值傳遞的(即沒有指針),並且所有的數據引用都是本地的自動棧變量(即沒有引用全局或靜態變量),那么函數就是顯示可重入的。也就是說,無論它是被如何調用的,我們都可以斷言它是可重入的。

隱式可重入函數

如果允許顯式可重入函數中的一些參數是引用傳遞的(即允許傳遞指針),那么就得到一個隱式可重入函數。也就是說,如果調用線程小心地傳遞指向非共享數據的指針,那么它是可重入的。例如上面的rand_r就是隱式可重入的。

通過上面的分析,有一個點值得注意:

  • 可重入性有時候既是調用者也是被調用者的屬性,並不只是被調用者單獨的屬性。

參考資料

  • RandalE.Bryant, DavidR.O’Hallaron, 布賴恩特,等. 深入理解計算機系統[M]. 機械工業出版社, 2011.
  • W.RichardStevens, StephenA.Rago, 史蒂文斯, 等. UNIX 環境高級編程 [M]. 人民郵電出版社, 2014.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM