線程局部存儲

爲什麼要有TLS?原因在於,進程中的全局變量與函數內定義的靜態(static)變量,是各個線程都可以訪問的共享變量。在一個線程修改的內存內容,對所有線程都生效。這是一個優點也是一個缺點。說它是優點,線程的數據交換變得非常快捷。說它是缺點,一個線程死掉了,其它線程也性命不保; 多個線程訪問共享數據,需要昂貴的同步開銷,也容易造成同步相關的BUG。

  如果需要在一個線程內部的各個函數調用都能訪問、但其它線程不能訪問的變量(被稱爲static memory local to a thread 線程局部靜態變量),就需要新的機制來實現。這就是TLS。

  線程局部存儲在不同的平臺有不同的實現,可移植性不太好。幸好要實現線程局部存儲並不難,最簡單的辦法就是建立一個全局表,通過當前線程ID去查詢相應的數據,因爲各個線程的ID不同,查到的數據自然也不同了。

  大多數平臺都提供了線程局部存儲的方法,無需要我們自己去實現:

  linux:

  int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));

  int pthread_key_delete(pthread_key_t key);

  void *pthread_getspecific(pthread_key_t key);

  int pthread_setspecific(pthread_key_t key, const void *value);

  Win32

  方法一:每個線程創建時系統給它分配一個LPVOID指針的數組(叫做TLS數組),這個數組從C編程角度是隱藏着的不能直接訪問,需要通過一些C API函數調用訪問。首先定義一些DWORD線程全局變量或函數靜態變量,準備作爲各個線程訪問自己的TLS數組的索引變量。一個線程使用TLS時,第一步在線程內調用TlsAlloc()函數,爲一個TLS數組索引變量與這個線程的TLS數組的某個槽(slot)關聯起來,例如獲得一個索引變量:

  global_dwTLSindex=TLSAlloc();

  注意,此步之後,當前線程實際上訪問的是這個TLS數組索引變量的線程內的拷貝版本。也就說,不同線程雖然看起來用的是同名的TLS數組索引變量,但實際上各個線程得到的可能是不同DWORD值。其意義在於,每個使用TLS的線程獲得了一個DWORD類型的線程局部靜態變量作爲TLS數組的索引變量。C/C++原本沒有直接定義線程局部靜態變量的機制,所以在如此大費周折。

  第二步,爲當前線程動態分配一塊內存區域(使用LocalAlloc()函數調用),然後把指向這塊內存區域的指針放入TLS數組相應的槽中(使用TlsValue()函數調用)。

  第三步,在當前線程的任何函數內,都可以通過TLS數組的索引變量,使用TlsGetValue()函數得到上一步的那塊內存區域的指針,然後就可以進行內存區域的讀寫操作了。這就實現了在一個線程內部這個範圍處處可訪問的變量。

  最後,如果不再需要上述線程局部靜態變量,要動態釋放掉這塊內存區域(使用LocalFree()函數),然後從TLS數組中放棄對應的槽(使用TlsFree()函數)。

在多線程程序中,經常要用全局變量來實現多個函數間的數據共享。由於數據空間是共享的,因此全局變量也爲所有進程共有。但有時應用程序設計中必要提供線程私有的全局變量,這個變量僅在線程中有效,但卻可以跨過多個函數訪問。

比如在程序裏可能需要每個線程維護一個鏈表,而會使用相同的函數來操作這個鏈表,最簡單的方法就是使用同名而不同變量地址的線程相關數據結構。這樣的數據結構可以由 Posix 線程庫維護,成爲線程私有數據 (Thread-specific Data,或稱爲 TSD)。

這裏主要測試和線程私有數據有關的 4 個函數:

pthread_key_create();
pthread_key_delete();

pthread_getspecific();
pthread_setspecific();

程序代碼

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
pthread_key_t key;
struct test_struct {
    int i;
    float k;
};
void *child1 (void *arg)
{
    struct test_struct struct_data;
    struct_data.i = 10;
    struct_data.k = 3.1415;
    pthread_setspecific (key, &struct_data);
    printf ("結構體struct_data的地址爲 0x%p\n", &(struct_data));
    printf ("child1 中 pthread_getspecific(key)返回的指針爲:0x%p\n", (struct test_struct *)pthread_getspecific(key));
    printf ("利用 pthread_getspecific(key)打印 child1 線程中與key關聯的結構體中成員值:\nstruct_data.i:%d\nstruct_data.k: %f\n", ((struct test_struct *)pthread_getspecific (key))->i, ((struct test_struct *)pthread_getspecific(key))->k);
    printf ("------------------------------------------------------\n");
}
void *child2 (void *arg)
{
    int temp = 20;
    sleep (2);
    printf ("child2 中變量 temp 的地址爲 0x%p\n",  &temp);
    pthread_setspecific (key, &temp);
    printf ("child2 中 pthread_getspecific(key)返回的指針爲:0x%p\n", (int *)pthread_getspecific(key));
    printf ("利用 pthread_getspecific(key)打印 child2 線程中與key關聯的整型變量temp 值:%d\n", *((int *)pthread_getspecific(key)));
}
int main (void)
{
    pthread_t tid1, tid2;
    pthread_key_create (&key, NULL);
    pthread_create (&tid1, NULL, (void *)child1, NULL);
    pthread_create (&tid2, NULL, (void *)child2, NULL);
    pthread_join (tid1, NULL);
    pthread_join (tid2, NULL);
    pthread_key_delete (key);
    return (0);
}


運行與輸出

 ./pthread_key 
結構體struct_data的地址爲 0x0xb7699388
child1 中 pthread_getspecific(key)返回的指針爲:0x0xb7699388
利用 pthread_getspecific(key)打印 child1 線程中與key關聯的結構體中成員值:
struct_data.i:10
struct_data.k: 3.141500
------------------------------------------------------
child2 中變量 temp 的地址爲 0x0xb6e9838c
child2 中 pthread_getspecific(key)返回的指針爲:0x0xb6e9838c


由輸出可見,pthread_getspecific() 返回的是與key 相關聯數據的指針。需要注意的是,在利用這個返回的指針時,它首先是 void 類型的,它雖然指向關聯的數據地址處,但並不知道指向的數據類型,所以在具體使用時,要對其進行強制類型轉換。
其次,兩個線程對自己的私有數據操作是互相不影響的。也就是說哦,雖然 key 是同名且全局,但訪問的內存空間並不是相同的一個。key 就像是一個數據管理員,線程的私有數據只是到他那去註冊,讓它知道你這個數據的存在。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章