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

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


咨詢
建站咨詢
