32-執行緒控制——執行緒特定資料
1. 執行緒特定資料
執行緒特定資料(thread-specific-data),也稱為執行緒私有資料(thread-private-data),是儲存和查詢某個特定執行緒相關資料的一種機制(APUE的說法)。
之所以稱為執行緒私有資料,是因為讓每個執行緒訪問屬於自己獨有的資料,這樣就不用關心執行緒同步的問題。比如執行緒id就是執行緒私有資料,為了防止與其他執行緒的資料混淆,需要進行一些保護。
但是一個程序中的所有執行緒幾乎可以訪問程序的整個地址空間,除了暫存器以外,一個執行緒是無法阻止其他執行緒訪問它的資料,即便是特定資料也不行。但是通過管理執行緒特定資料的函式可以提高執行緒間資料的獨立性,儘量做到阻止執行緒訪問其他執行緒的資料。
2. 程序的key陣列和執行緒的pthread結構
POSIX規定為每個程序維護了一個key結構的容器(可以理解為一個數組),通過這個陣列可以獲取或儲存執行緒私有資料,這個陣列中的每個結構稱之為一個執行緒特定資料元素
,POSIX規定系統實現的Key結構陣列必須包含不少於128個執行緒特定元素。
每個執行緒的執行緒特定資料元素至少包含兩項內容:標誌和解構函式指標
,key結構的的標誌表示這個陣列元素是否使用,解構函式指標表示執行緒退出時釋放執行緒特定資料。
當一個執行緒呼叫pthread_key_create函式建立一個key時,系統就會從key陣列中關聯一個未使用的key。建立的key通過pthread_key_create函式的引數keyp指標返回該key的地址,第二個引數是一個函式指標,指向一個解構函式。
除了程序內key陣列,系統還為程序內的每個執行緒維護了一個執行緒結構,也就是pthread結構。pthread結構維護的pkey指標是和程序的key陣列的所有key相關聯,指標指向的記憶體是執行緒特定資料。
具體的過程如上圖所示:
當啟動一個程序並建立了若干執行緒,其中一個執行緒要申請執行緒特定資料,呼叫了pthread_key_create函式在程序中的key結構陣列
中找到一個未使用的元素,並把key返回給呼叫者,假設返回的這個key索引號為0,執行緒呼叫pthread_getspecific函式把本執行緒的pkey[0]值與程序的key陣列的key[0]相關聯,返回的是一個空指標pkey = NULL,而這個pkey指標就是指向實際的執行緒特定資料的首地址了
此時現在為空,需要通過malloc分配記憶體,在呼叫pthread_setspecific()呼叫將執行緒特定資料的指標指向剛分配的記憶體塊,整個過程如上圖所示。
3. 執行緒特定資料操作函式
在使用執行緒特定資料前,需要建立與特定資料關聯的鍵,通過這個鍵可以訪問執行緒特定資料,通過pthread_key_create函式進行建立一個鍵值,鍵的資料型別為pthread_key_t。
int pthread_key_create(pthread_key_t *key, void (*destructor(void*));
引數key:建立返回的鍵值 引數destructor:是一個函式指標,指向一個解構函式,執行緒退出時會呼叫該解構函式。
比如下面這個函式就是引數2,線上程退出時會自動被呼叫:
void destructor(void* arg) {
...
}
pthread_key_delete函式用於刪除指定的鍵與執行緒特定資料之間的關聯關係。
int pthread_key_delete(pthread_key_t key);
需要注意的是,呼叫pthread_key_delete不會引起前面的解構函式呼叫。
pthread_setpecific函式用於根據鍵值去設定(關聯)執行緒特定資料。
int pthread_setspecific(pthread_key_t key, const void *value);
引數key:指定設定的鍵值 引數value:執行緒特定資料的記憶體首地址
該函式用於根據鍵值去獲取執行緒特定資料
void *pthread_getspecific(pthread_key_t key);
返回值:成功返回執行緒特定資料,失敗則該鍵值沒有關聯。
4. 執行緒特定資料例項
該程式建立兩個執行緒,設定並獲取各自的執行緒特定資料。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
//定義鍵值
pthread_key_t key;
//解構函式
void destructor(void *arg){
printf("destructor data = %d\n\n",(int)arg);
}
//執行緒1
void *tfn1(void *arg){
//執行緒特定資料
int data = 110;
//設定執行緒特定資料
int ret = pthread_setspecific(key , (void *)data);
if(ret != 0){
perror("pthread_setspecific");
pthread_exit(NULL);
}
//獲取執行緒特定資料
int res = (int)pthread_getspecific(key);
printf("tid1: data = %d\n",res); //輸出執行緒特定資料
printf("tid1 will going to die\n");
return (void *)0;
}
//執行緒2
void *tfn2(void *arg){
//執行緒特定資料
int data = 999;
//設定執行緒特定資料
int ret = pthread_setspecific(key , (void *)data);
if(ret != 0){
perror("pthread_setspecific");
pthread_exit(NULL);
}
//獲取執行緒特定資料
int res = (int)pthread_getspecific(key);
printf("tid2: data = %d\n",res); //輸出執行緒特定資料
printf("tid2 will going to die\n");
return (void *)0;
}
int main(void){
pthread_t tid1 , tid2;
int ret;
//關聯鍵值
ret = pthread_key_create(&key , destructor);
if(ret != 0){
perror("pthread_key_create");
return -1;
}
//建立2個執行緒
ret = pthread_create(&tid1 , NULL , tfn1 , NULL);
if(ret != 0){
perror("pthread_create");
}
ret = pthread_create(&tid2 , NULL , tfn2 , NULL);
if(ret != 0){
perror("pthread_create");
}
pthread_join(tid1 , NULL);
pthread_join(tid2 , NULL);
return 0;
}
程式執行結果:
程式建立了執行緒1和執行緒2,執行緒主控函式設定了各自的執行緒特定資料,然後又獲取了各自的資料,執行緒退出前列印going to ide提示,緊接著會呼叫解構函式釋放執行緒的資源,且解構函式中的值就是執行緒的特定資料。
5. pthread_once機制
在前面的執行緒特定資料示例中,假設因為執行緒競爭的關係導致多次呼叫pthread_key_create函式的話,可能會出現一些不可預料的情況,比如有些執行緒可能看到一個key值,其他執行緒看到的可能是不同的key值。
為了避免出現這種情況,我們希望呼叫pthread_key_create函式初始化的時候只執行一次。
POSIX pthread提供了pthread_once,如果我們想要對某些資料只初始化一次就可以使用pthread_once了。
int phtread_once(pthread_once_t *initflag, void (*initfn)(void));
返回值:若成功返回0,若失敗返回錯誤編號
引數initflag:指定控制變數(once_ocntrol),用於記錄初始化函式的執行狀態
引數initfn:是 一個函式指標,就是指定的初始化函式,函式型別為void (*initfn)(void)
使用pthread_once需要注意兩點:
- 定義一個pthread_once_t型別的全域性控制變數,必須使用PTHREAD_ONCE_INIT巨集來進行初始化。
- 還需要定義一個初始化函式,比如pthread_init函式。
pthread_once函式首先會檢查控制變數(once_control)的初值,判斷是否已經完成初始化,如果完成就簡單返回,否則pthread_once呼叫初始化函式,控制變數會記錄初始化完成。如果已經有一個執行緒在初始化時,其他執行緒再呼叫pthread_once會阻塞等待,直到那個執行緒初始化完成才返回。
//once_control控制變數值必須指定為PTHREAD_ONCE_INIT
pthread_once_t once_control = PTHREAD_ONCE_INIT;
這樣pthread_once指定的初始化函式只執行一次,而once_control控制變量表示初始化函式是否執行過,具體是哪個執行緒執行初始化函式這是不確定的。
6. 執行緒特定資料例項改進版
通過pthread_once機制對執行緒特定資料例項進行改寫
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
//定義控制變數,初始化必須為PTHREAD_ONCE_INIT
pthread_once_t once_control = PTHREAD_ONCE_INIT;
//定義鍵值
pthread_key_t key;
//定義解構函式
void destructor(void *arg){
printf("destructor data = %d\n\n" , (int)arg);
}
//初始化函式
void pthread_init(void){
//建立,初始化(在此初始化)
printf("pthread_once is running\n");
pthread_key_create(&key , destructor);
}
void *tfn1(void *arg){
int data = 110;
pthread_setspecific(key , (void *)data);
int res = (int)pthread_getspecific(key);
printf("pthread1: data = %d\n" , res);
return (void *)0;
}
void *tfn2(void *arg){
int data = 120;
pthread_setspecific(key , (void *)data);
int res = (int)pthread_getspecific(key);
printf("pthread2: data = %d\n" , res);
return (void *)0;
}
int main(void){
pthread_t tid1 , tid2;
//呼叫pthread_once函式
pthread_once(&once_control , pthread_init);
pthread_create(&tid1 , NULL , tfn1 , NULL);
pthread_create(&tid2 , NULL , tfn2 , NULL);
pthread_join(tid1 , NULL);
pthread_join(tid2 , NULL);
return 0;
}
執行結果:
7. 執行緒特定資料操作函式注意事項
-
如果執行緒中呼叫了exit,_exit等函式,或出現了非正常退出,則不會呼叫解構函式。
-
如果執行緒的特定資料使用了malloc分配記憶體,需要在解構函式中釋放記憶體,否則將出現記憶體洩漏