有了信號處理程序和線程,多個控制線程在同一時間可能潛在地調用同一個函數。
如果一個函數在同一時刻可以被多個線程安全地調用,就稱該函數是線程安全的。在Single UNIX Specification中定義的所有函數,除了表12-5中列出的函數以外,其他函數都保證是線程安全的。另外,ctermid和tmpnam函數在參數傳入空指針時并不能保證是線程安全的。類似地,wcrtomb和wcsrtombs函數如果參數mbstate_t傳入的是空指針的話,也不能保證它們是線程安全的。
表12-5 POSIX.1中不能保證線程安全的函數(此表摘自第三版)
支持線程安全函數的操作系統實現會在<unistd.h>中定義符號_POSIX_THREAD_SAFE_FUNCTIONS。應用程序可以在sysconf函數中傳入_SC_THREAD_SAFE_FUNCTIONS參數,以在運行時檢查是否支持線程安全函數。所有遵循XSI的實現要求必須支持線程安全函數。
操作系統實現支持線程安全函數這一特性時,對POSIX.1中的一些非線程安全函數,它會提供可替代的線程安全版本,表12-6列出了這些函數的線程安全版本。很多函數并不是線程安全的,因為他們返回的數據是存放在靜態的內存緩沖區中。通過修改接口,要求調用者自己提供緩沖區可以使函數變為線程安全的。
表12-6 替代的線程安全函數(此表摘自第三版)
表12-6中列出的函數的命名方式與他們的非線程安全版本的名字相似,只不過在名字最后加了_r,以表明這個版本是可重入的。
如果一個函數對多個線程來說是可重入的,則說這個函數是線程安全的,但這并不能說明對信號處理程序來說該函數也是可重入的。如果函數對異步信號處理程序的重入是安全的,那么就可以說函數是異步-信號安全的。
除了表12-6中列出的函數,POSIX.1還提供了以線程安全的方式管理FILE對象的方法。可以使用flockfile和ftrylockfile獲取與給定FILE對象關聯的鎖。這個鎖是遞歸的,當占有這把鎖的時候,還可以再次獲取該鎖,這并不會導致死鎖。雖然這種鎖的具體實現并無規定,但要求所有操作FILE對象的標準I/O例程表現得就像它們內部調用了flockfile和funlockfile一樣。
#include <stdio.h>int ftrylockfile(FILE *fp);返回值:若成功則返回0,否則返回非0值void flockfile(FILE *fp);void funlockfile(FILE *fp);
雖然標準的I/O例程從它們各自的內部數據結構這一角度出發,可能是以線程安全的方式實現的,但有時把鎖開放給應用程序仍然是非常有用的。這允許應用程序把多個對標準I/O函數的調用組合成原子序列。當然,在處理多個FILE對象時,需要注意可能出現的死鎖,并且需要對所有的鎖仔細地排序。
如果標準I/O例程都獲取它們各自的鎖,那么在做一次一個字符的I/O操作時性能就會出現嚴重的下降。在這種情況下,需要對每一個字符的讀或寫操作進行獲取鎖和釋放鎖的動作。為了避免這種開銷,出現了不加鎖版本的基于字符的標準I/O例程。
#include <stdio.h>int getchar_unlocked(void);int getc_unlocked(FILE *fp);兩者的返回值都是:若成功則返回一個字符,若已到達文件結尾或出錯則返回EOFint putchar_unlocked(int c);int putc_unlocked(int c, FILE *fp);兩者的返回值都是:若成功則返回c,若出錯則返回EOF
除非被flockfile(或ftrylockfile)和funlockfile的調用包圍,否則盡量不要調用這四個函數,因為它們會導致不可預期的結果(即由多個控制線程非同步地訪問數據所引起的種種問題)。
一旦對FILE對象進行加鎖,就可以在釋放鎖之前對這些函數進行多次調用。這樣就可以在多次的數據讀寫上分攤總的加解鎖的開銷。
實例
程序清單12-3顯示了getenv(http://www.CUOXin.com/nufangrensheng/p/3508319.html)一個可能的實現。因為所有調用getenv的線程返回的字符串都存放在同一個靜態緩沖區中,所以這個版本不是可重入的。如果兩個線程同時調用這個函數,就會看到不一致的結果。
程序清單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函數來確保每個進程只調用一次thread_init函數。
程序清單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可重入,需要改變接口,調用者必須自己提供緩沖區,這樣每個線程可以使用各自不同的緩沖區從而避免其他線程的干擾。但是注意這還不足以使getenv_r成為線程安全的,要使getenv_r成為線程安全的,需要在搜索請求的字符串時保護環境不被修改。我們可以使用互斥量,通過getenv_r和putenv函數對環境列表的訪問進行序列化。
可以使用讀寫鎖,從而允許對getenv_r的多次并發訪問,但并發性的增強可能并不會在很大程度上改善程序的性能。這里面有兩個原因:首先,環境列表通常不會很長,所以掃描列表時并不需要長時間地占有互斥量;其次,對getenv和putenv的調用不是頻繁發生的,所以改善它們的性能并不會對程序的整體性能產生很大的影響。
即使把getenv_r變成線程安全的,也并不意味著它對信號處理程序是可重入的。如果使用的是非遞歸的互斥量,當線程從信號處理程序中調用getenv_r時,就有可能出現死鎖。如果信號處理程序在線程執行getenv_r時中斷了該線程,由于這時已經占有加鎖的env_mutex,這樣其他線程試圖對這個互斥量的加鎖就會被阻塞,最終導致線程進入死鎖狀態。所以,必須使用遞歸互斥量阻止其他線程改變當前正在查看的數據結構,同時還要阻止來自信號處理程序的死鎖。問題是pthread函數并不保證是異步信號安全的,所以不能把pthread函數用于其他函數,讓該函數成為異步信號安全的。
本篇博文內容摘自《UNIX環境高級編程》(第二版),僅作個人學習記錄所用。關于本書可參考:http://www.apuebook.com/。
新聞熱點
疑難解答