執行緒特定資料TSD總結
一、執行緒的本質
Linux執行緒又稱輕量程序(LWP),也就說執行緒本質是用程序之間共享使用者空間模擬實現的。
二、執行緒模型的引入
執行緒模型引入是為了資料共享,為什麼又引入執行緒私有資料?有時候想讓基於程序的介面適應多執行緒環境,這時候就需要為每個執行緒維護一份私有資料了,最典型的就是errno了。
在維護每個執行緒的私有資料的時候,我們可能會想到分配一個儲存執行緒資料的陣列,用執行緒的ID作為陣列的索引來實現訪問。
1. 系統生成的執行緒ID不能保證是一個小而連續的整數
2. 用陣列實現的時候容易出現越界讀寫的情況
鑑於這兩個問題,我們可以藉助執行緒的私有資料(TSD)來解決這個問題。
三、執行緒特定資料
執行緒私有資料(Thread Specific Data),是儲存和查詢與某個執行緒相關的資料的一種機制。把這種資料稱為執行緒私有資料或執行緒特定資料的原因是,希望每個執行緒可以獨立地訪問資料副本,從而不需要考慮多執行緒同步問題。
提示:程序中的所有執行緒都可以訪問程序的整個地址空間。除了使用暫存器以外(一個執行緒真正擁有的唯一私有儲存是處理器的暫存器),執行緒沒有辦法阻止其他執行緒訪問它的資料,執行緒私有資料也不例外。雖然底層的實現部分並不能阻止這種訪問能力,但管理執行緒私有資料的函式可以提高執行緒間的資料獨立性。
四、關鍵函式說明
int pthread_key_create(pthread_key_t *keyp,void (*destructor)(void *));
返回值:若成功則返回0,否則返回錯誤編號
功能:建立的鍵存放在keyp指向的記憶體單元,這個鍵可以被程序中的所有執行緒使用,但每個執行緒把這個鍵與不同的執行緒私有資料地址進行關聯。
說明:建立一個執行緒私有資料鍵,必須保證對於每個
Pthread_key_t
變數僅僅被呼叫一次,因為如果一個鍵被建立兩次,其實是在建立兩個不同的鍵。第二個鍵將覆蓋第一個鍵,第一個鍵以及任何執行緒可能與其關聯的執行緒私有資料值將丟失。解決這種競爭的辦法是使用pthread_once
。pthread_once_t initflag = PTHREAD_ONCE_INIT
;
int pthread_once(pthread_once_t *initflag, void (*initfn)(void));
返回值:若成功則返回0,否則返回錯誤編號
說明:initflag必須是一個非本地變數(即全域性變數或靜態變數),而且必須初始化為PTHREAD_ONCE_INIT。當執行緒呼叫
pthread_exit
或者執行緒執行返回,正常退出時,如果私有資料不為空且註冊了解構函式,解構函式就會被呼叫,但如果執行緒呼叫了exit
、_exit
、_Exit
、abort
或出現其他非正常的退出時就不會呼叫解構函式注1 。- 執行緒可以為執行緒私有資料分配多個鍵注2,每個鍵都可以有一個解構函式與它關聯。各個鍵的解構函式可以互不相同,當然它們也可以使用相同的解構函式。
- 執行緒退出時,執行緒私有資料的解構函式將按照作業系統實現中定義的順序被呼叫。解構函式可能會呼叫另一個函式,該函式可能會建立新的執行緒私有資料而且把這個資料與當前的鍵關聯起來。當所有的解構函式都呼叫完成以後,系統會檢查是否還有非null的執行緒私有資料值與鍵關聯,如果有的話,再次呼叫解構函式。這個過程會一直重複直到執行緒所有的鍵都為null值執行緒私有資料,或者已經做了最大次數的嘗試注3。
- 建立新鍵時,每個執行緒的資料地址設為NULL。
int pthread_key_delete(pthread_key_t *keyp);
返回值:若成功則返回0,否則返回錯誤編號
功能:取消鍵與執行緒私有資料值之間的關聯關係。
說明:- 呼叫
pthread_delete
不會啟用與鍵關聯的解構函式,容易造成記憶體洩露。要釋放任何與鍵對應的執行緒私有資料值的記憶體空間,需要在應用程式中採取額外的步驟。當刪除執行緒私有資料鍵的時候,不會影響任何執行緒對該鍵設定的執行緒私有資料值,甚至不影響呼叫執行緒當前鍵值。建議最後才刪除執行緒私有資料鍵,尤其當一些執行緒仍然持有該鍵的值時,就更不該釋放該鍵。使用已經刪除的私有資料鍵將導致未定義的行為。
- 呼叫
五、刨根問底啥原理
執行緒私有資料實現的主要思想,如圖所示
系統內部為每個程序維護了兩種資料,pthread_key_struct
結構體陣列和pthread
結構體:
1. pthread_key_struct結構的“標誌”指示這個資料元素是否正在使用,當然在剛開始時所有的標誌初始化為“不在使用”;
2. pthread結構體中有一部分內容是我們稱之為pkey陣列的一個128個元素的指標陣列;
3. 在分配執行緒私有資料之前需要呼叫pthread_key_create
建立與該資料相關聯的健。系統搜尋pthread_key_struct
結構陣列,找出第一個“不在使用”的元素,並把該元素的索引(0~127)稱為“鍵”,返回給呼叫執行緒的正是這個索引。這個鍵可以被程序中的所有執行緒使用,每個執行緒把這個鍵與不同的執行緒私有資料地址進行關聯(個人愚見:此處的關聯指的就是使用相同的索引)。雖然索引值相同,但是由於各個執行緒pkey陣列的起始地址不同,結果導致每個執行緒根據相同的索引取得的值不同(該值就是存放的私有資料)。
六、私有資料使用示例
/*
* TSD(Thread Specific Data)使用步驟說明
* 執行環境:SlackwareLinux 64bit
*/
#include <stdio.h>
#include <pthread.h>
//1、建立一個型別為 pthread_key_t 型別的變數
pthread_key_t key;
void destructor(void *arg)
{
//arg即為儲存的執行緒資料
printf("destructor executed in thread %lx, param = %lx\n", pthread_self(), arg);
}
void * child1(void *arg)
{
pthread_t tid = pthread_self();
printf("thread1 %lx entering\n", tid);
//3、進行執行緒資料儲存。
//param1為前面宣告的 pthread_key_t key,
//param2為要儲存的資料, void*型別說明可以儲存任何型別的資料
pthread_setspecific(key, (void *)tid);
sleep(2); //讓出cpu
//4、取出所儲存的執行緒資料。
//param為前面提到的 pthread_key_t key
//如果沒有執行緒私有資料值與鍵關聯,pthread_getspecific將返回一個空指標,可以據此來確定是否需要呼叫pthread_setspecific。
printf("thread1 %lx returned %lx\n", tid, pthread_getspecific(key));
}
void *child2(void *arg)
{
pthread_t tid = pthread_self();
printf("thread2 %lx entering\n", tid);
pthread_setspecific(key, (void *)tid);
sleep(1);
printf("thread2 %lx returned %lx\n", tid, pthread_getspecific(key));
}
int main(int argc, char *argv[])
{
pthread_t tid1, tid2;
printf("main thread %lx entering\n", pthread_self());
//2、把key與不同的執行緒私有資料地址進行關聯
//第一個引數就是步驟1中宣告的key的地址;
//第二個引數是一個清理執行緒儲存的函式,不是動態申請的該函式指標可以設成 NULL;
pthread_key_create(&key, destructor);
pthread_create(&tid1, NULL, child1, NULL);
pthread_create(&tid2, NULL, child2, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
//5、解除key與執行緒私有資料地址的關聯
pthread_key_delete(key);
printf("main thread %lx returned\n", pthread_self());
return 0;
}
執行結果
[email protected]:/scratchbox/test/lidonghai# ./a.out
main thread b77336c0 entering
thread1 b7732b90 entering
thread2 b6f32b90 entering
thread2 b6f32b90 returned b6f32b90
destructor executed in thread b6f32b90, param = b6f32b90
thread1 b7732b90 returned b7732b90
destructor executed in thread b7732b90, param = b7732b90
main thread b77336c0 returned
/*
* 三個執行緒:主執行緒,th1,th2各自有自己的私有資料區域
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
static pthread_key_t str_key; //建立一個型別為 pthread_key_t 型別的變數
static pthread_once_t str_alloc_key_once = PTHREAD_ONCE_INIT; //define a static variable that only be allocated once
static void str_alloc_key();
static void str_alloc_destroy_accu(void* accu);
char* str_accumulate(const char* s)
{
char* accu;
pthread_once(&str_alloc_key_once,str_alloc_key); //str_alloc_key()這個函式只調用一次
accu = (char*)pthread_getspecific(str_key); //取得該執行緒對應的關鍵字所關聯的私有資料空間首址
if(accu==NULL){ //每個新剛建立的執行緒這個值一定是NULL(沒有指向任何已分配的資料空間)
accu=malloc(1024);
if(!accu) return NULL;
accu[0] = 0;
pthread_setspecific(str_key,(void*)accu);//設定該執行緒對應的關鍵字關聯的私有資料空間
printf("Thread %lx: allocating buffer at %p\n",pthread_self(),accu);
}
strcat(accu,s);
return accu;
}
static void str_alloc_key()
{
pthread_key_create(&str_key,str_alloc_destroy_accu);//建立關鍵字及其對應的記憶體釋放函式,當程序建立關鍵字後,這個關鍵字是NULL。
printf("Thread %lx: allocated key %d\n",pthread_self(),str_key);
}
static void str_alloc_destroy_accu(void* accu)
{
printf("Thread %lx: freeing buffer at %p\n",pthread_self(),accu);
free(accu);
}
//執行緒入口函式
void* process(void *arg)
{
char* res;
res=str_accumulate("Result of ");
if(strcmp((char*)arg,"first")==0)
sleep(3);
res=str_accumulate((char*)arg);
res=str_accumulate(" thread");
printf("Thread %lx: \"%s\"\n",pthread_self(),res);
return NULL;
}
//主執行緒函式
int main(int argc,char* argv[])
{ char* res;
pthread_t th1,th2;
res=str_accumulate("Result of ");
pthread_create(&th1,NULL,process,(void*)"first");
pthread_create(&th2,NULL,process,(void*)"second");
res=str_accumulate("initial thread");
printf("Thread %lx: \"%s\"\n",pthread_self(),res);
pthread_join(th1,NULL);
pthread_join(th2,NULL);
pthread_exit(0);
}
執行結果
[[email protected]10h57 c]# ./pthread_private_data
Thread b7fdd6c0 : allocated key 0
Thread b7fdd6c0: allocating buffer at 0x911c008
Thread b7fdd6c0: "Result of initial thread"
Thread b7fdcb90: allocating buffer at 0x911c938
Thread b75dbb90: allocating buffer at 0x911cd40
Thread b75dbb90: "Resule of second thread"
Thread b75dbb90: freeing buffer at 0x911cd40
Thread b7fdcb90: "Resule of first thread"
Thread b7fdcb90: freeing buffer at 0x911c938
Thread b7fdd6c0: freeing buffer at 0x911c008