轉載請說明出處:/content/6643580.html
在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 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實現。
三、線程局部數據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*轉換成原始數據類型的指針。
四、深入理解線程局部存儲機制 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