線程私有數據(也稱線程特定數據)是存儲和查詢與某個線程相關的數據的一種機制。把這種數據稱為線程私有數據或線程特定數據的原因是:希望每個線程可以獨立地訪問數據副本,而不需要擔心與其他線程的同步訪問問題。
線程模型促進了進程中數據和屬性的共享,許多人在設計線程模型時會遇到各種麻煩。但在這樣的模型中,為什么還需要提出一些合適的用於阻止共享的接口呢?其中有兩個原因:
第一,有時候需要維護基於每個線程的數據。
采用線程私有數據的第二個原因是:它提供了讓基於進程的接口適應多線程環境的機制。一個很明顯的實例就是errno。回憶http://www.cnblogs.com/nufangrensheng/p/3495426.html中對errno的討論,(線程出現)以前的接口把errno定義為進程環境中全局可訪問的整數。系統調用和庫例程在調用或執行失敗時設置errno,把它作為操作失敗時的附屬結果。為了讓線程也能夠使用那些原本基於進程的的系統調用和庫例程,errno被重新定義為線程私有數據。這樣,一個線程做了設置errno的操作並不會影響進程中其他線程的errno的值。
進程中的所有線程都可以訪問進程的整個地址空間。除了使用寄存器以外,線程沒有辦法阻止其他線程訪問它的數據,線程私有數據也不例外。雖然底層的實現部分並不能阻止這種訪問能力,但管理線程私有數據的函數可以提高線程間的數據獨立性。
在分配線程私有數據之前,需要創建與該數據關聯的鍵。這個鍵將用於獲取對線程私有數據的訪問權。使用pthread_key_create創建一個鍵。
#include <pthread.h> int pthread_key_create(pthread_key_t *keyp, void (*destructor)(void *)); 返回值:若成功則返回0,否則返回錯誤編號
創建的鍵存放在keyp指向的內存單元,這個鍵可以被進程中的所有線程使用,但每個線程把這個鍵與不同的線程私有數據地址進行關聯(如何關聯???)。創建新鍵時,每個線程的數據地址設為null值。
除了創建鍵以外,pthread_key_create可以選擇為該鍵關聯析構函數,當線程退出時,如果數據地址已經被置為非null數值,那么析構函數就會被調用,它唯一的參數就是該數據地址。如果傳入的destructor參數為null,就表明沒有析構函數與鍵關聯。當線程調用pthread_exit或者線程執行返回,正常退出時,析構函數就會被調用,但如果線程調用了exit、_exit、_Exit、abort或出現其他非正常的退出時(關於正常退出與不正常退出:http://www.cnblogs.com/nufangrensheng/p/3509618.html),就不會調用析構函數。
線程通常使用malloc為線程私有數據分配內存空間,析構函數通常釋放已分配的內存。如果線程沒有釋放內存就退出了,那么這塊內存將會丟失,即線程所屬進程出現了內存泄漏。
線程可以為線程私有數據分配多個鍵,每個鍵都可以有一個析構函數與它關聯。各個鍵的析構函數可以互不相同,當然它們也可以使用相同的析構函數。每個操作系統在實現的時候可以對進程可分配的鍵的數量進行限制(回憶http://www.cnblogs.com/nufangrensheng/p/3522577.html中表12-1中的PTHREAD_KEYS_MAX)。
線程退出時,線程私有數據的析構函數將按照操作系統實現中定義的順序被調用。析構函數可能會調用另一個函數,該函數可能會創建新的線程私有數據而且把這個數據與當前的鍵關聯起來。當所有的析構函數都調用完成以后,系統會檢查是否還有非null的線程私有數據值與鍵關聯,如果有的話,再次調用析構函數。這個過程會一直重復直到線程所有的鍵都為null值線程私有數據,或者已經做了PTHREAD_DESTRUCTOR_ITERATIONS(http://www.cnblogs.com/nufangrensheng/p/3522577.html中表12-1)中定義的最大次數的嘗試。
對所有的線程,都可以通過調用pthread_key_delete來取消鍵與線程私有數據值之間的關聯關系。
#include <pthread.h> int pthread_key_delete(pthread_key_t *key); 返回值:若成功則返回0,否則返回錯誤編號
注意,調用pthread_key_delete並不會激活與鍵關聯的析構函數。要釋放任何與鍵對應的線程私有數據值的內存空間,需要在應用程序中采取額外的步驟。
需要確保分配的鍵並不會由於在初始化階段的競爭而發生變動。下列代碼可以導致兩個線程都調用pthread_key_create:
void destructor(void *); pthread_key_t key; int init_done = 0; int threadfunc(void *arg) { if(!init_done) { init_done = 1; err = pthread_key_create(&key, destructor); } ... }
有些線程可能看到某個鍵值,而其他的線程看到的可能是另一個不同的鍵值,這取決於系統是如何調度線程的,解決這種競爭的辦法是使用pthread_once。
#include <pthread.h> pthread_once_t initflag = PTHREAD_ONCE_INIT; int pthread_once(pthread_once_t *initflag, void (*initfn)(void)); 返回值:若成功則返回0,否則返回錯誤編號
initflag必須是一個非本地變量(即全局變量或靜態變量),而且必須初始化為PTHREAD_ONCE_INIT。
如果每個線程都調用pthread_once,系統就能保證初始化例程initfn只被調用一次,即在系統首次調用pthread_once時。創建鍵時避免出現競爭的一個恰當的方法可以描述如下:
void destructor(void *); pthread_key_t key; thread_once_t init_done = PTHREAD_ONCE_INIT; void thread_init(void) { err = pthread_key_create(&key, destructor); } int threadfunc(void *arg) { pthread_once(&init_done, thread_init); ... }
鍵一旦創建,就可以通過pthread_setspecific函數把鍵和線程私有數據關聯起來。可以通過pthread_getspecific函數獲得線程私有數據的地址。
#include <pthread.h> void *pthread_getspecific(pthread_key_t key); 返回值:線程私有數據值;若沒有值與鍵關聯則返回NULL int pthread_setspecific(pthread_key_t key, const void *value); 返回值:若成功則返回0,否則返回錯誤編號
如果沒有線程私有數據值與鍵關聯,pthread_getspecific將返回一個空指針,可以據此來確定是否需要調用pthread_setspecific。
實例
程序清單12-5 線程安全的getenv的兼容版本(使用線程私有數據來維護每個線程的數據緩沖區的副本,用於存放各自的返回字符串)
#include <limits.h> #include <string.h> #include <pthread.h> #include <stdlib.h> static pthread_key_t key; static pthread_once_t init_done = PTHREAD_ONCE_INIT; pthread_mutex_t env_mutex = PTHREAD_MUTEX_INITIALIZER; extern char **environ; static void pthread_init(void) { pthread_key_create(&key, free); } char * getenv(const char *name) { int i, len; char *envbuf; pthread_once(&init_done, thread_init); pthread_mutex_lock(&env_mutex); envbuf = (char *)pthread_getspecific(key); if(envbuf == NULL) { envbuf = malloc(ARG_MAX); if(envbuf == NULL) { pthread_mutex_unlock(&env_mutex); return(NULL); } pthread_setspecific(key, envbuf); } 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]); pthread_mutex_unlock(&env_mutex); return(envbuf); } } pthread_mutex_unlock(&env_mutex); return(NULL); }
使用pthread_once來確保只為將要使用的線程私有數據創建了一個鍵。如果pthread_getspecific返回的是空指針,需要分配內存然后把鍵與該內存單元關聯,否則如果返回的不是空指針,就是用pthread_getspecific返回的內存單元。對析構函數,使用free來釋放之前由malloc分配的內存。只有當線程私有數據值為非null時,析構函數才會被調用。
注意,雖然這個版本的getenv是線程安全的,但它並不是異步-信號安全的。對信號處理程序而言,即使使用遞歸的互斥量,這個版本的getenv也不可能是可重入的,因為它調用了malloc,而malloc函數本身並不是異步-信號安全的。
本篇博文內容摘自《UNIX環境高級編程》(第二版),僅作個人學習記錄所用。關於本書可參考:http://www.apuebook.com/。