Linux中的線程局部存儲(轉)

轉載請說明出處:http://blog.csdn.net/cywosp/article/details/26469435git

 

   在Linux系統中使用C/C++進行多線程編程時,咱們遇到最多的就是對同一變量的多線程讀寫問題,大多狀況下遇到這類問題都是經過鎖機制來處理,但這對程序的性能帶來了很大的影響,固然對於那些系統原生支持原子操做的數據類型來講,咱們可使用原子操做來處理,這能對程序的性能會獲得必定的提升。那麼對於那些系統不支持原子操做的自定義數據類型,在不使用鎖的狀況下如何作到線程安全呢?本文將從線程局部存儲方面,簡單講解處理這一類線程安全問題的方法。
 
1、數據類型
    在C/C++程序中常存在全局變量、函數內定義的靜態變量以及局部變量,對於局部變量來講,其不存在線程安全問題,所以不在本文討論的範圍以內。全局變量和函數內定義的靜態變量,是同一進程中各個線程均可以訪問的共享變量,所以它們存在多線程讀寫問題。在一個線程中修改了變量中的內容,其餘線程都能感知而且能讀取已更改過的內容,這對數據交換來講是很是快捷的,可是因爲多線程的存在,對於同一個變量可能存在兩個或兩個以上的線程同時修改變量所在的內存內容,同時又存在多個線程在變量在修改的時去讀取該內存值,若是沒有使用相應的同步機制來保護該內存的話,那麼所讀取到的數據將是不可預知的,甚至可能致使程序崩潰。
    若是須要在一個線程內部的各個函數調用都能訪問、但其它線程不能訪問的變量,這就須要新的機制來實現,咱們稱之爲Static memory local to a thread (線程局部靜態變量),同時也可稱之爲線程特有數據(TSD: Thread-Specific Data)或者線程局部存儲(TLS: Thread-Local Storage)。這一類型的數據,在程序中每一個線程都會分別維護一份變量的副本(copy),而且長期存在於該線程中,對此類變量的操做不影響其餘線程。以下圖:
                                   
  
2、一次性初始化
   在講解線程特有數據以前,先讓咱們來了解一下一次性初始化。多線程程序有時有這樣的需求:無論建立多少個線程,有些數據的初始化只能發生一次。列如:在C++程序中某個類在整個進程的生命週期內只能存在一個實例對象,在多線程的狀況下,爲了能讓該對象可以安全的初始化,一次性初始化機制就顯得尤其重要了。——在設計模式中這種實現經常被稱之爲單例模式(Singleton)。Linux中提供了以下函數來實現一次性初始化:
#include <pthread.h>
 
// Returns 0 on success, or a positive error number on error
int pthread_once (pthread_once_t *once_control, void (*init) (void));
利用參數once_control的狀態,函數pthread_once()能夠確保不管有多少個線程調用多少次該函數,也只會執行一次由init所指向的由調用者定義的函數。init所指向的函數沒有任何參數,形式以下:
void init (void)
{
   // some variables initializtion in here
}

另外,參數once_control必須是pthread_once_t類型變量的指針,指向初始化爲PTHRAD_ONCE_INIT的靜態變量。在C++0x之後提供了相似功能的函數std::call_once (),用法與該函數相似。使用實例請參考https://github.com/ApusApp/Swift/blob/master/swift/base/singleton.hpp實現。github

 
3、線程局部數據API
    在Linux中提供了以下函數來對線程局部數據進行操做

#include <pthread.h>
 
// Returns 0 on success, or a positive error number on error
int pthread_key_create (pthread_key_t *key, void (*destructor)(void *));
 
// Returns 0 on success, or a positive error number on error
int pthread_key_delete (pthread_key_t key);
 
// Returns 0 on success, or a positive error number on error
int pthread_setspecific (pthread_key_t key, const void *value);
 
// Returns pointer, or NULL if no thread-specific data is associated with key
void *pthread_getspecific (pthread_key_t key);

 

函數pthread_key_create()爲線程局部數據建立一個新鍵,並經過key指向新建立的鍵緩衝區。由於全部線程均可以使用返回的新鍵,因此參數key能夠是一個全局變量(在C++多線程編程中通常不使用全局變量,而是使用單獨的類對線程局部數據進行封裝,每一個變量使用一個獨立的pthread_key_t)。destructor所指向的是一個自定義的函數,其格式以下:
void Dest (void *value)
{
    // Release storage pointed to by 'value'
}

 

只要線程終止時與key關聯的值不爲NULL,則destructor所指的函數將會自動被調用。若是一個線程中有多個線程局部存儲變量,那麼對各個變量所對應的destructor函數的調用順序是不肯定的,所以,每一個變量的destructor函數的設計應該相互獨立。
 
函數pthread_key_delete()並不檢查當前是否有線程正在使用該線程局部數據變量,也不會調用清理函數destructor,而只是將其釋放以供下一次調用pthread_key_create()使用。在Linux線程中,它還會將與之相關的線程數據項設置爲NULL。
因爲系統對每一個進程中pthread_key_t類型的個數是有限制的,因此進程中並不能建立無限個的pthread_key_t變量。Linux中能夠經過PTHREAD_KEY_MAX(定義於limits.h文件中)或者系統調用sysconf(_SC_THREAD_KEYS_MAX)來肯定當前系統最多支持多少個鍵。Linux中默認是1024個鍵,這對於大多數程序來講已經足夠了。若是一個線程中有多個線程局部存儲變量,一般能夠將這些變量封裝到一個數據結構中,而後使封裝後的數據結構與一個線程局部變量相關聯,這樣就能減小對鍵值的使用。
 
函數pthread_setspecific()用於將value的副本存儲於一數據結構中,並將其與調用線程以及key相關聯。參數value一般指向由調用者分配的一塊內存,當線程終止時,會將該指針做爲參數傳遞給與key相關聯的destructor函數。當線程被建立時,會將全部的線程局部存儲變量初始化爲NULL,所以第一次使用此類變量前必須先調用pthread_getspecific()函數來確認是否已經於對應的key相關聯,若是沒有,那麼pthread_getspecific()會分配一塊內存並經過pthread_setspecific()函數保存指向該內存塊的指針。
參數value的值也能夠不是一個指向調用者分配的內存區域,而是任何能夠強制轉換爲void*的變量值,在這種狀況下,先前的pthread_key_create()函數應將參數
destructor設置爲NULL
函數pthread_getspecific()正好與pthread_setspecific()相反,其是將pthread_setspecific()設置的value取出。在使用取出的值前最好是將void*轉換成原始數據類型的指針。
 
4、深刻理解線程局部存儲機制
    1. 深刻理解線程局部存儲的實現有助於對其API的使用。在典型的實現中包含如下數組:
  • 一個全局(進程級別)的數組,用於存放線程局部存儲的鍵值信息
pthread_key_create()返回的pthread_key_t類型值只是對全局數組的索引,該全局數組標記爲pthread_keys,其格式大概以下:
                          
數組的每一個元素都是一個包含兩個字段的結構,第一個字段標記該數組元素是否在用,第二個字段用於存放針對此鍵、線程局部存儲變的解構函數的一個副本,即destructor函數。
  • 每一個線程還包含一個數組,存有爲每一個線程分配的線程特有數據塊的指針(經過調用pthread_setspecific()函數來存儲的指針,即參數中的value)
   2. 在常見的存儲pthread_setspecific()函數參數value的實現中,大多數都相似於下圖的實現。圖中假設pthread_keys[1]分配給func1()函數,pthread API爲每一個函數維護指向線程局部存儲數據塊的一個指針數組,其中每一個數組元素都與圖線程局部數據鍵的實現(上圖)中的全局pthread_keys中元素一一對應。
                     
 
5、總結
    使用全局變量或者靜態變量是致使多線程編程中非線程安全的常見緣由。在多線程程序中,保障非線程安全的經常使用手段之一是使用互斥鎖來作保護,這種方法帶來了併發性能降低,同時也只能有一個線程對數據進行讀寫。若是程序中能避免使用全局變量或靜態變量,那麼這些程序就是線程安全的,性能也能夠獲得很大的提高。若是有些數據只能有一個線程能夠訪問,那麼這一類數據就可使用線程局部存儲機制來處理,雖然使用這種機制會給程序執行效率上帶來必定的影響,但對於使用鎖機制來講,這些性能影響將能夠忽略。Linux C++的線程局部存儲簡單實現可參考https://github.com/ApusApp/Swift/blob/master/swift/base/threadlocal.h,更詳細且高效的實現可參考Facebook的folly庫中的ThreadLocal實現。更高性能的線程局部存儲機制就是使用__thread,這將在下一節中討論。
 
__下一節:
 在Linux中還有一種更爲高效的線程局部存儲方法,就是使用關鍵字__thread來定義變量。__thread是GCC內置的線程局部存儲設施(Thread-Local Storage),它的實現很是高效,與pthread_key_t向比較更爲快速,其存儲性能能夠與全局變量相媲美,並且使用方式也更爲簡單。建立線程局部變量只需簡單的在全局或者靜態變量的聲明中加入__thread說明便可。列如:
    static __thread char t_buf[32] = {'\0'};
    extern __thread int t_val = 0;
凡是帶有__thread的變量,每一個線程都擁有該變量的一份拷貝,且互不干擾。線程局部存儲中的變量將一直存在,直至線程終止,當線程終止時會自動釋放這一存儲。__thread並非全部數據類型均可以使用的,由於其只支持POD(Plain old data structure)[1]類型,不支持class類型——其不能自動調用構造函數和析構函數。同時__thread能夠用於修飾全局變量、函數內的靜態變量,可是不能用於修飾函數的局部變量或者class的普通成員變量。另外,__thread變量的初始化只能用編譯期常量,例如:
    __thread std::string t_object_1 ("Swift");                   // 錯誤,由於不能調用對象的構造函數
    __thread std::string* t_object_2 = new std::string (); // 錯誤,初始化必須用編譯期常量
    __thread std::string* t_object_3 = nullptr;                // 正確,可是須要手工初始化並銷燬對象

 

除了以上以外,關於線程局部存儲變量的聲明和使用還需注意一下幾點:
  1. 若是變量聲明中使用量關鍵字static或者extern,那麼關鍵字__thread必須緊隨其後。
  2. 與通常的全局變量或靜態變量同樣,線程局部變量在聲明時能夠設置一個初始化值。
  3. 可使用C語言取地址符(&)來獲取線程局部變量的地址。
__thread的使用例子可參考https://github.com/ApusApp/Swift/blob/master/swift/base/logging.cpp的實現及其單元測試對於那些非POD數據類型,若是想使用線程局部存儲機制,可使用對pthread_key_t封裝的類來處理,具體方式可參考https://github.com/ApusApp/Swift/blob/master/swift/base/threadlocal.h的實現以及其的單元測試
 
 
參考
[1] http://zh.wikipedia.org/wiki/POD_(%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1)
[2] Linux/UNIX系統編程手冊(上)
[3] Linux多線程服務端編程使用muduo C++網絡庫
相關文章
相關標籤/搜索