【Linux】Linux執行緒私有資料
執行緒私有資料
在單執行緒程式中,函式經常使用全域性變數或靜態變數,這是不會影響程式的正確性的,但如果執行緒呼叫的函式使用全域性變數或靜態變數,則很可能引起錯誤。因為這些函式使用的全域性變數和靜態變數無法為不同的執行緒儲存各自的值,而當同一程序內的不同執行緒幾乎同時呼叫這樣的函式時就可能會有問題發生。
而解決這一問題的一種方式就是使用執行緒私有資料。執行緒私有資料採用了一種被稱為一鍵多值的技術,即一個鍵對應多個數值。訪問資料時都是通過鍵值來訪問,好像是對一個變數進行訪問,其實是在訪問不同的資料。使用執行緒私有資料時,首先要為每個執行緒資料建立一個相關聯的鍵。在各個執行緒內部,都使用這個公用的鍵來指代執行緒資料,但是在不同的執行緒中,這個鍵代表的資料是不同的。
這和JAVA中的ThreadLocal變數的思想有點像:儘管是一個變數,但是在不同的執行緒中呼叫就是取出的不同的值。
設計執行緒私有資料的原因:
- 在維護每個執行緒的私有資料的時候,我們可能會想到分配一個儲存執行緒資料的陣列,用執行緒的ID作為陣列的索引來實現訪問,但是有一個問題是:系統生成的執行緒ID不能保證是一個小而連續的整數, 並且用陣列實現的時候由於其他執行緒也可以訪問其陣列中的資料,這樣會引起資料的不安全;
- 執行緒私有資料提供了讓基於程序的介面適應多執行緒環境的機制。
一個很明顯的例項就是errno,以前的介面(執行緒出現以前)把errno定義為程序上下文中全域性可訪問的整數。系統呼叫和庫例程在呼叫或執行失敗時設定 errno,把它作為操作失敗的附屬結果。為了讓執行緒也能夠使用那些原本基於程序的系統呼叫和庫例程,errno 被重定義為執行緒私有資料。這樣,一個執行緒做了重置errno的操作也不會影響程序中其他執行緒的errno值。
執行緒私有變數函式
執行緒私有變數的相關函式為:
執行緒私有變數的初始化
int pthread_key_create (pthread_key_t *key, void (*destructor)(void *));
函式說明:key引數為指向一個鍵值的指標,第二個引數指明瞭一個destructor函式,如果這個引數不為空,那麼當每個執行緒結束時,系統將呼叫這個函式來釋放繫結在這個鍵上的記憶體塊。
銷燬執行緒私有變數
int pthread_key_delete (pthread_key_t key);
函式說明:key引數為一個鍵值。
向執行緒私有變數賦值
int pthread_setspecific (pthread_key_t key, const void *value);
函式說明:value引數是不同的執行緒中,key值所關聯的私有資料地址,這些地址可以用malloc來分配。
獲取執行緒私有變數
void *pthread_getspecific (pthread_key_t key);
函式說明:key引數為一個鍵值,返回值是執行緒私有變數的資料地址。
指定函式只執行一次
int phread_once(pthread_once_t *onceptr, void(*init)(void));
函式說明:本函式使用初值為PTHREAD_ONCE_INIT的once_control變數保證init_routine()函式在本程序執行序列中僅執行一次。注意的是,onceptr必須是一個非本地變數(即全域性變數或者靜態變數),而且必須初始化為PTHREAD_ONCE_INIT。
一般使用環境:在多執行緒環境中,有些事僅需要執行一次。通常當初始化應用程式時,可以比較容易地將其放在main函式中。但當你寫一個庫時,就不能在main裡面初始化了,你可以用靜態初始化,但使用一次初始化(pthread_once)會比較容易些。
例子:
pthread_key_t key;
pthread_once_t r1_once = PTHREAD_ONCE_INIT;
void destructor(void *ptr){
free(ptr);
}
void excute_once(void) {
pthread_key_create(&key, destructor);
}
int main(){
pthread_once(&r1_once, excute_once);
}
TSD池
作為TSD池,就是執行緒私有資料(Thread-specific Data)池。
當我們呼叫pthread_key_create()時,核心從Linux的TSD池中分配一項,將其值賦給key供以後訪問使用,它的第一個引數key為指向鍵值的指標,第二個引數為一個函式指標,如果指標不為空,則線上程退出時將以key所關聯的資料為引數呼叫destr_function(),釋放分配的緩衝區。 key一旦被建立,所有執行緒都可以訪問它,但各執行緒可以根據自己的需要往key中填入不同的值,這就相當於提供了一個同名而不同值的全域性變數,一鍵多值。其中TSD池的結構如下:
核心支援的pthread_keys是有限的(一般是128),除了程序範圍內的keys結構陣列之外,系統還在程序內維護了關於多個執行緒的多條資訊。這些特定於執行緒的資訊我們稱之為Pthread結構。其中部分內容是我們稱之為pkey陣列的一個128個元素的指標陣列。系統維護的關於每個執行緒的資訊結構圖如下:
一旦我們在某個執行緒裡面呼叫pthread_setspecific()將key與某個空間(這個空間可以是malloc申請的)相關連的時候,上面的結構就變成:
實際例子
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
pthread_key_t key;
void echomsg( void* param )
{
printf( "destructor excuted in thread %lu, param = %lu\n", pthread_self(), *((unsigned long int*)param) );
}
void* child1( void* param )
{
unsigned long int tid = pthread_self();
printf( "thread %lu enter\n", tid );
pthread_setspecific( key, ( void* )tid );
sleep( 2 );
unsigned long int val = *((unsigned long int *)pthread_getspecific( key ));
printf( "thread %lu returns %lu\n", tid, val );
sleep( 5 );
return ( void* )0;
}
void* child2( void* param )
{
unsigned long int tid = pthread_self();
printf( "thread %lu enter\n", tid );
pthread_setspecific( key, ( void* )tid );
sleep( 1 );
unsigned long int val = *( (unsigned long int*)pthread_getspecific( key ) );
printf( "thread %lu returns %lu\n", tid, val );
sleep( 5 );
return ( void* )0;
}
int main()
{
pthread_t tid1, tid2;
printf( "main thread enter\n" );
pthread_key_create( &key, echomsg );
pthread_create( &tid1, NULL, child1, NULL );
pthread_create( &tid2, NULL, child2, NULL );
sleep( 10 );
pthread_key_delete( key );
printf( "main thread exit\n" );
return 0;
}
通過g++編譯命令:
g++ -lpthread -o TSD.out TSD.cpp
最終的執行結果為:
kennie@cbib:~/pthreadDIR$ ./TSD.out
main thread enter
thread 3075607408 enter
thread 3067214704 enter
thread 3067214704 returns 3067214704
thread 3075607408 returns 3075607408
destructor excuted in thread 3067214704, param = 3067214704
destructor excuted in thread 3075607408, param = 3075607408
main thread exit