新聞中心
在Linux系統(tǒng)中使用C/C++進行多線程編程時,我們遇到最多的就是對同一變量的多線程讀寫問題,大多情況下遇到這類問題都是通過鎖機制來處理,但這對程序的性能帶來了很大的影響,當然對于那些系統(tǒng)原生支持原子操作的數(shù)據(jù)類型來說,我們可以使用原子操作來處理,這能對程序的性能會得到一定的提高。那么對于那些系統(tǒng)不支持原子操作的自定義數(shù)據(jù)類型,在不使用鎖的情況下如何做到線程安全呢?本文將從線程局部存儲方面,簡單講解處理這一類線程安全問題的方法。

申扎網站制作公司哪家好,找創(chuàng)新互聯(lián)!從網頁設計、網站建設、微信開發(fā)、APP開發(fā)、成都響應式網站建設公司等網站項目制作,到程序開發(fā),運營維護。創(chuàng)新互聯(lián)從2013年成立到現(xiàn)在10年的時間,我們擁有了豐富的建站經驗和運維經驗,來保證我們的工作的順利進行。專注于網站建設就選創(chuàng)新互聯(lián)。
一、數(shù)據(jù)類型
在C/C++程序中常存在全局變量、函數(shù)內定義的靜態(tài)變量以及局部變量,對于局部變量來說,其不存在線程安全問題,因此不在本文討論的范圍之內。全局變量和函數(shù)內定義的靜態(tài)變量,是同一進程中各個線程都可以訪問的共享變量,因此它們存在多線程讀寫問題。在一個線程中修改了變量中的內容,其他線程都能感知并且能讀取已更改過的內容,這對數(shù)據(jù)交換來說是非??旖莸?,但是由于多線程的存在,對于同一個變量可能存在兩個或兩個以上的線程同時修改變量所在的內存內容,同時又存在多個線程在變量在修改的時去讀取該內存值,如果沒有使用相應的同步機制來保護該內存的話,那么所讀取到的數(shù)據(jù)將是不可預知的,甚至可能導致程序崩潰。 如果需要在一個線程內部的各個函數(shù)調用都能訪問、但其它線程不能訪問的變量,這就需要新的機制來實現(xiàn),我們稱之為Static memory local to a thread (線程局部靜態(tài)變量),同時也可稱之為線程特有數(shù)據(jù)(TSD: Thread-Specific Data)或者線程局部存儲(TLS: Thread-Local Storage)。這一類型的數(shù)據(jù),在程序中每個線程都會分別維護一份變量的副本(copy),并且長期存在于該線程中,對此類變量的操作不影響其他線程。如下圖:
二、一次性初始化
在講解線程特有數(shù)據(jù)之前,先讓我們來了解一下一次性初始化。多線程程序有時有這樣的需求:不管創(chuàng)建多少個線程,有些數(shù)據(jù)的初始化只能發(fā)生一次。列如:在C++程序中某個類在整個進程的生命周期內只能存在一個實例對象,在多線程的情況下,為了能讓該對象能夠安全的初始化,一次性初始化機制就顯得尤為重要了?!谠O計模式中這種實現(xiàn)常常被稱之為單例模式(Singleton)。Linux中提供了如下函數(shù)來實現(xiàn)一次性初始化:
#include
// Returns 0 on success, or a positive error number on error
int pthread_once (pthread_once_t *once_control, void (*init) (void));
利用參數(shù)once_control的狀態(tài),函數(shù)pthread_once()可以確保無論有多少個線程調用多少次該函數(shù),也只會執(zhí)行一次由init所指向的由調用者定義的函數(shù)。init所指向的函數(shù)沒有任何參數(shù),形式如下:
void init (void)
{
// some variables initializtion in here
}
另外,參數(shù)once_control必須是pthread_once_t類型變量的指針,指向初始化為PTHRAD_ONCE_INIT的靜態(tài)變量。在C++0x以后提供了類似功能的函數(shù)std::call_once (),用法與該函數(shù)類似。
三、線程局部數(shù)據(jù)API
在Linux中提供了如下函數(shù)來對線程局部數(shù)據(jù)進行操作
#include
// 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);
函數(shù)pthread_key_create()為線程局部數(shù)據(jù)創(chuàng)建一個新鍵,并通過key指向新創(chuàng)建的鍵緩沖區(qū)。因為所有線程都可以使用返回的新鍵,所以參數(shù)key可以是一個全局變量(在C++多線程編程中一般不使用全局變量,而是使用單獨的類對線程局部數(shù)據(jù)進行封裝,每個變量使用一個獨立的pthread_key_t)。destructor所指向的是一個自定義的函數(shù),其格式如下:
void Dest (void *value)
{
// Release storage pointed to by 'value'
}
只要線程終止時與key關聯(lián)的值不為NULL,則destructor所指的函數(shù)將會自動被調用。如果一個線程中有多個線程局部存儲變量,那么對各個變量所對應的destructor函數(shù)的調用順序是不確定的,因此,每個變量的destructor函數(shù)的設計應該相互獨立。 函數(shù)pthread_key_delete()并不檢查當前是否有線程正在使用該線程局部數(shù)據(jù)變量,也不會調用清理函數(shù)destructor,而只是將其釋放以供下一次調用pthread_key_create()使用。在Linux線程中,它還會將與之相關的線程數(shù)據(jù)項設置為NULL。 由于系統(tǒng)對每個進程中pthread_key_t類型的個數(shù)是有限制的,所以進程中并不能創(chuàng)建無限個的pthread_key_t變量。Linux中可以通過PTHREAD_KEY_MAX(定義于limits.h文件中)或者系統(tǒng)調用sysconf(_SC_THREAD_KEYS_MAX)來確定當前系統(tǒng)最多支持多少個鍵。Linux中默認是1024個鍵,這對于大多數(shù)程序來說已經足夠了。如果一個線程中有多個線程局部存儲變量,通常可以將這些變量封裝到一個數(shù)據(jù)結構中,然后使封裝后的數(shù)據(jù)結構與一個線程局部變量相關聯(lián),這樣就能減少對鍵值的使用。 函數(shù)pthread_setspecific()用于將value的副本存儲于一數(shù)據(jù)結構中,并將其與調用線程以及key相關聯(lián)。參數(shù)value通常指向由調用者分配的一塊內存,當線程終止時,會將該指針作為參數(shù)傳遞給與key相關聯(lián)的destructor函數(shù)。當線程被創(chuàng)建時,會將所有的線程局部存儲變量初始化為NULL,因此第一次使用此類變量前必須先調用pthread_getspecific()函數(shù)來確認是否已經于對應的key相關聯(lián),如果沒有,那么pthread_getspecific()會分配一塊內存并通過pthread_setspecific()函數(shù)保存指向該內存塊的指針。 參數(shù)value的值也可以不是一個指向調用者分配的內存區(qū)域,而是任何可以強制轉換為void的變量值,在這種情況下,先前的pthread_key_create()函數(shù)應將參數(shù) destructor設置為NULL 函數(shù)pthread_getspecific()正好與pthread_setspecific()相反,其是將pthread_setspecific()設置的value取出。在使用取出的值前最好是將void轉換成原始數(shù)據(jù)類型的指針。
四、深入理解線程局部存儲機制
\1. 深入理解線程局部存儲的實現(xiàn)有助于對其API的使用。在典型的實現(xiàn)中包含以下數(shù)組: pthread_key_create()返回的pthread_key_t類型值只是對全局數(shù)組的索引,該全局數(shù)組標記為pthread_keys,其格式大概如下:
數(shù)組的每個元素都是一個包含兩個字段的結構,第一個字段標記該數(shù)組元素是否在用,第二個字段用于存放針對此鍵、線程局部存儲變的解構函數(shù)的一個副本,即destructor函數(shù)。 \2. 在常見的存儲pthread_setspecific()函數(shù)參數(shù)value的實現(xiàn)中,大多數(shù)都類似于下圖的實現(xiàn)。圖中假設pthread_keys[1]分配給func1()函數(shù),pthread API為每個函數(shù)維護指向線程局部存儲數(shù)據(jù)塊的一個指針數(shù)組,其中每個數(shù)組元素都與圖線程局部數(shù)據(jù)鍵的實現(xiàn)(上圖)中的全局pthread_keys中元素一一對應。
五、總結
使用全局變量或者靜態(tài)變量是導致多線程編程中非線程安全的常見原因。在多線程程序中,保障非線程安全的常用手段之一是使用互斥鎖來做保護,這種方法帶來了并發(fā)性能下降,同時也只能有一個線程對數(shù)據(jù)進行讀寫。如果程序中能避免使用全局變量或靜態(tài)變量,那么這些程序就是線程安全的,性能也可以得到很大的提升。如果有些數(shù)據(jù)只能有一個線程可以訪問,那么這一類數(shù)據(jù)就可以使用線程局部存儲機制來處理,雖然使用這種機制會給程序執(zhí)行效率上帶來一定的影響,但對于使用鎖機制來說,這些性能影響將可以忽略。更高性能的線程局部存儲機制就是使用__thread,這個以后再討論。
當前文章:詳解Linux線程局部存儲
轉載來源:http://m.fisionsoft.com.cn/article/ccehchs.html


咨詢
建站咨詢
