轉載請說明出處:http://blog.csdn.net/cywosp/article/details/26469435
在Linux系統中使用C/C++進行多線程編程時,我們遇到最多的就是對同一變數的多線程讀寫問題,大多情況下遇到這類問題都是通過鎖機制來處理,但這對程式的效能帶來了很大的影響,當然對於那些系統原生支援原子操作的資料類型來說,我們可以使用原子操作來處理,這能對程式的效能會得到一定的提高。那麼對於那些系統不支援原子操作的自訂資料類型,在不使用鎖的情況下如何做到安全執行緒呢?本文將從線程局部儲存方面,簡單講解處理這一類安全執行緒問題的方法。
一、資料類型 在C/C++程式中常存在全域變數、函數內定義的靜態變數以及局部變數,對於局部變數來說,其不存線上程安全問題,因此不在本文討論的範圍之內。全域變數和函數內定義的靜態變數,是同一進程中各個線程都可以訪問的共用變數,因此它們存在多線程讀寫問題。在一個線程中修改了變數中的內容,其他線程都能感知並且能讀取已更改過的內容,這對資料交換來說是非常快捷的,但是由於多線程的存在,對於同一個變數可能存在兩個或兩個以上的線程同時修改變數所在的記憶體內容,同時又存在多個線程在變數在修改的時去讀取該記憶體值,如果沒有使用相應的同步機制來保護該記憶體的話,那麼所讀取到的資料將是不可預知的,甚至可能導致程式崩潰。 如果需要在一個線程內部的各個函數調用都能訪問、但其它線程不能訪問的變數,這就需要新的機制來實現,我們稱之為Static memory local to a thread (線程局部靜態變數),同時也可稱之為線程特有資料(TSD: Thread-Specific Data)或者線程局部儲存(TLS: Thread-Local Storage)。這一類型的資料,在程式中每個線程都會分別維護一份變數的副本(copy),並且長期存在於該線程中,對此類變數的操作不影響其他線程。如:
二、一次性初始化 在講解線程特有資料之前,先讓我們來瞭解一下一次性初始化。多線程程式有時有這樣的需求:不管建立多少個線程,有些資料的初始化只能發生一次。列如:在C++程式中某個類在整個進程的生命週期內只能存在一個執行個體對象,在多線程的情況下,為了能讓該對象能夠安全的初始化,一次性初始化機制就顯得尤為重要了。——在設計模式中這種實現常常被稱之為單例模式(Singleton)。Linux中提供了如下函數來實現一次性初始化:
#include <pthread.h>
// Returns 0 on success, or a positive error number on errorint 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實現。
三、線程局部資料API 在Linux中提供了如下函數來對線程局部資料進行操作
#include <pthread.h>
// Returns 0 on success, or a positive error number on errorint pthread_key_create (pthread_key_t *
key, void (*
destructor)(void *));
// Returns 0 on success, or a positive error number on errorint pthread_key_delete (pthread_key_t
key);
// Returns 0 on success, or a positive error number on errorint pthread_setspecific (pthread_key_t
key, const void *
value);
// Returns pointer, or NULL if no thread-specific data is associated with keyvoid *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*轉換成未經處理資料類型的指標。
四、深入理解線程局部儲存機制 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中元素一一對應。
五、總結 使用全域變數或者靜態變數是導致多線程編程中非安全執行緒的常見原因。在多線程程式中,保障非安全執行緒的常用手段之一是使用互斥鎖來做保護,這種方法帶來了並發效能下降,同時也只能有一個線程對資料進行讀寫。如果程式中能避免使用全域變數或靜態變數,那麼這些程式就是安全執行緒的,效能也可以得到很大的提升。如果有些資料只能有一個線程可以訪問,那麼這一類資料就可以使用線程局部儲存機制來處理,雖然使用這種機制會給程式執行效率上帶來一定的影響,但對於使用鎖機制來說,這些效能影響將可以忽略。Linux C++的線程局部儲存簡單實現可參考https://github.com/ApusApp/Swift/blob/master/swift/base/threadlocal.h,更詳細且高效的實現可參考Facebook的folly庫中的ThreadLocal實現。更高效能的線程局部儲存機制就是使用__thread,這將在下一節中討論。
參考:[1] Linux/UNIX系統編程手冊(上)[2] http://www.groad.net/bbs/thread-2182-1-1.html[3] http://baike.baidu.com/view/598128.htm