有了信號處理程序和線程,多個控制線程在同一時間可能潛在地調(diào)用同一個函數(shù)。
如果一個函數(shù)在同一時刻可以被多個線程安全地調(diào)用,就稱該函數(shù)是線程安全的。在Single UNIX Specification中定義的所有函數(shù),除了表12-5中列出的函數(shù)以外,其他函數(shù)都保證是線程安全的。另外,ctermid和tmpnam函數(shù)在參數(shù)傳入空指針時并不能保證是線程安全的。類似地,wcrtomb和wcsrtombs函數(shù)如果參數(shù)mbstate_t傳入的是空指針的話,也不能保證它們是線程安全的。
表12-5 POSIX.1中不能保證線程安全的函數(shù)(此表摘自第三版)
支持線程安全函數(shù)的操作系統(tǒng)實(shí)現(xiàn)會在<unistd.h>中定義符號_POSIX_THREAD_SAFE_FUNCTIONS。應(yīng)用程序可以在sysconf函數(shù)中傳入_SC_THREAD_SAFE_FUNCTIONS參數(shù),以在運(yùn)行時檢查是否支持線程安全函數(shù)。所有遵循XSI的實(shí)現(xiàn)要求必須支持線程安全函數(shù)。
操作系統(tǒng)實(shí)現(xiàn)支持線程安全函數(shù)這一特性時,對POSIX.1中的一些非線程安全函數(shù),它會提供可替代的線程安全版本,表12-6列出了這些函數(shù)的線程安全版本。很多函數(shù)并不是線程安全的,因為他們返回的數(shù)據(jù)是存放在靜態(tài)的內(nèi)存緩沖區(qū)中。通過修改接口,要求調(diào)用者自己提供緩沖區(qū)可以使函數(shù)變?yōu)榫€程安全的。
表12-6 替代的線程安全函數(shù)(此表摘自第三版)
表12-6中列出的函數(shù)的命名方式與他們的非線程安全版本的名字相似,只不過在名字最后加了_r,以表明這個版本是可重入的。
如果一個函數(shù)對多個線程來說是可重入的,則說這個函數(shù)是線程安全的,但這并不能說明對信號處理程序來說該函數(shù)也是可重入的。如果函數(shù)對異步信號處理程序的重入是安全的,那么就可以說函數(shù)是異步-信號安全的。
除了表12-6中列出的函數(shù),POSIX.1還提供了以線程安全的方式管理FILE對象的方法。可以使用flockfile和ftrylockfile獲取與給定FILE對象關(guān)聯(lián)的鎖。這個鎖是遞歸的,當(dāng)占有這把鎖的時候,還可以再次獲取該鎖,這并不會導(dǎo)致死鎖。雖然這種鎖的具體實(shí)現(xiàn)并無規(guī)定,但要求所有操作FILE對象的標(biāo)準(zhǔn)I/O例程表現(xiàn)得就像它們內(nèi)部調(diào)用了flockfile和funlockfile一樣。
#include <stdio.h>int ftrylockfile(FILE *fp);返回值:若成功則返回0,否則返回非0值void flockfile(FILE *fp);void funlockfile(FILE *fp);
雖然標(biāo)準(zhǔn)的I/O例程從它們各自的內(nèi)部數(shù)據(jù)結(jié)構(gòu)這一角度出發(fā),可能是以線程安全的方式實(shí)現(xiàn)的,但有時把鎖開放給應(yīng)用程序仍然是非常有用的。這允許應(yīng)用程序把多個對標(biāo)準(zhǔn)I/O函數(shù)的調(diào)用組合成原子序列。當(dāng)然,在處理多個FILE對象時,需要注意可能出現(xiàn)的死鎖,并且需要對所有的鎖仔細(xì)地排序。
如果標(biāo)準(zhǔn)I/O例程都獲取它們各自的鎖,那么在做一次一個字符的I/O操作時性能就會出現(xiàn)嚴(yán)重的下降。在這種情況下,需要對每一個字符的讀或?qū)懖僮鬟M(jìn)行獲取鎖和釋放鎖的動作。為了避免這種開銷,出現(xiàn)了不加鎖版本的基于字符的標(biāo)準(zhǔn)I/O例程。
#include <stdio.h>int getchar_unlocked(void);int getc_unlocked(FILE *fp);兩者的返回值都是:若成功則返回一個字符,若已到達(dá)文件結(jié)尾或出錯則返回EOFint putchar_unlocked(int c);int putc_unlocked(int c, FILE *fp);兩者的返回值都是:若成功則返回c,若出錯則返回EOF
除非被flockfile(或ftrylockfile)和funlockfile的調(diào)用包圍,否則盡量不要調(diào)用這四個函數(shù),因為它們會導(dǎo)致不可預(yù)期的結(jié)果(即由多個控制線程非同步地訪問數(shù)據(jù)所引起的種種問題)。
一旦對FILE對象進(jìn)行加鎖,就可以在釋放鎖之前對這些函數(shù)進(jìn)行多次調(diào)用。這樣就可以在多次的數(shù)據(jù)讀寫上分?jǐn)偪偟募咏怄i的開銷。
實(shí)例
程序清單12-3顯示了getenv(http://www.CUOXin.com/nufangrensheng/p/3508319.html)一個可能的實(shí)現(xiàn)。因為所有調(diào)用getenv的線程返回的字符串都存放在同一個靜態(tài)緩沖區(qū)中,所以這個版本不是可重入的。如果兩個線程同時調(diào)用這個函數(shù),就會看到不一致的結(jié)果。
程序清單12-3 getenv的非可重入版本
#include <limits.h>#include <signal.h>static char envbuf[ARG_MAX];extern char **environ;char *getenv(const char *name);{ int i, len; len = strlen(name); for(i = 0; environ[i] != NULL; i++) { if((strncmp(name, environ[i], len) == 0) && (environ[i][len] == '=')) { strcpy(envbuf, &environ[i][len+1]); return(envbuf); } } return(NULL);}
程序清單12-4給出了getenv的可重入版本,這個版本命名為getenv_r。它使用pthread_once函數(shù)來確保每個進(jìn)程只調(diào)用一次thread_init函數(shù)。
程序清單12-4 getenv的可重入(線程安全)版本
#include <string.h>#include <errno.h>#include <pthread.h>#include <stdlib.h>extern char **environ;pthread_mutex_t env_mutex;static pthread_once_t init_done = PTHREAD_ONCE_INIT;static voidthread_init(void){ pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init(&env_mutex, &attr); pthread_mutexattr_destroy(&attr);}intgetenv_r(const char *name, char *buf, int buflen){ int i, len, olen; pthread_once(&init_done, thread_init); len = strlen(name); pthread_mutex_lock(&env_mutex); for(i = 0; environ[i][len] != NULL; i++) { if((strncmp(name, environ[i], len) == 0) && (environ[i][len] == '=')) { olen = strlen(&environ[i][len+1]); if(olen >= buflen) { pthread_mutex_unlock(&env_mutex); return(ENOSPC); } strcpy(buf, &environ[i][len+1]); pthread_mutex_unlock(&env_mutex); return(0); } } pthread_mutex_unlock(&env_mutex); return(ENOENT);}
要使getenv_r可重入,需要改變接口,調(diào)用者必須自己提供緩沖區(qū),這樣每個線程可以使用各自不同的緩沖區(qū)從而避免其他線程的干擾。但是注意這還不足以使getenv_r成為線程安全的,要使getenv_r成為線程安全的,需要在搜索請求的字符串時保護(hù)環(huán)境不被修改。我們可以使用互斥量,通過getenv_r和putenv函數(shù)對環(huán)境列表的訪問進(jìn)行序列化。
可以使用讀寫鎖,從而允許對getenv_r的多次并發(fā)訪問,但并發(fā)性的增強(qiáng)可能并不會在很大程度上改善程序的性能。這里面有兩個原因:首先,環(huán)境列表通常不會很長,所以掃描列表時并不需要長時間地占有互斥量;其次,對getenv和putenv的調(diào)用不是頻繁發(fā)生的,所以改善它們的性能并不會對程序的整體性能產(chǎn)生很大的影響。
即使把getenv_r變成線程安全的,也并不意味著它對信號處理程序是可重入的。如果使用的是非遞歸的互斥量,當(dāng)線程從信號處理程序中調(diào)用getenv_r時,就有可能出現(xiàn)死鎖。如果信號處理程序在線程執(zhí)行g(shù)etenv_r時中斷了該線程,由于這時已經(jīng)占有加鎖的env_mutex,這樣其他線程試圖對這個互斥量的加鎖就會被阻塞,最終導(dǎo)致線程進(jìn)入死鎖狀態(tài)。所以,必須使用遞歸互斥量阻止其他線程改變當(dāng)前正在查看的數(shù)據(jù)結(jié)構(gòu),同時還要阻止來自信號處理程序的死鎖。問題是pthread函數(shù)并不保證是異步信號安全的,所以不能把pthread函數(shù)用于其他函數(shù),讓該函數(shù)成為異步信號安全的。
本篇博文內(nèi)容摘自《UNIX環(huán)境高級編程》(第二版),僅作個人學(xué)習(xí)記錄所用。關(guān)于本書可參考:http://www.apuebook.com/。
新聞熱點(diǎn)
疑難解答
圖片精選