在單線程程序中,我們經(jīng)常要使用全局變量
來實現(xiàn)多個函數(shù)間共享數(shù)據(jù)。在多線程環(huán)境下,由于數(shù)據(jù)空間是共享的目胡,因此全局變量也為所有線程所共有。但有時在應(yīng)用程序設(shè)計中有必要提供線程私有
的全局變量诚隙,僅在某個線程中有效讶隐,但可以跨多個函數(shù)訪問,這樣每個線程訪問它自己獨立的數(shù)據(jù)空間久又,而不用擔(dān)心和其它線程的同步訪問巫延。
比如:在程序中每個線程都使用同一個指針?biāo)饕粋€鏈表,并在多個函數(shù)內(nèi)通過指針對鏈表進行操作地消,但是每個線程通過指針?biāo)饕逆湵矶际亲约邯氂械臄?shù)據(jù)炉峰。
線程特有數(shù)據(jù)(Thread Specific Data)
這樣在一個線程內(nèi)部的各個函數(shù)都能訪問、但其它線程不能訪問的變量脉执,我們就需要使用線程局部靜態(tài)變量
(Static memory local to a thread) 同時也可稱之為線程特有數(shù)據(jù)
(Thread-Specific Data 或 TSD)疼阔,或者線程局部存儲
(Thread-Local Storage 或 TLS)。
POSIX
線程庫提供了如下 API 來管理線程特有數(shù)據(jù)(TSD):
1. 創(chuàng)建 key
int pthread_key_create(pthread_key_t *key, void (*destructor)(void *))半夷;
第一參數(shù) key
指向 pthread_key_t
的對象的指針婆廊。請注意這里 pthread_key_t
的對象占用的空間是用戶事先分配好的,pthread_key_create
不會動態(tài)生成 pthread_key_t
對象巫橄。
第二參數(shù) desctructor
淘邻,如果這個參數(shù)不為空,那么當(dāng)每個線程結(jié)束時湘换,系統(tǒng)將調(diào)用這個函數(shù)來釋放綁定在這個鍵上的內(nèi)存塊宾舅。
2. 動態(tài)數(shù)據(jù)初始化
有時我們在線程里初始化時,需要避免重復(fù)初始化彩倚。我們希望一個線程里只調(diào)用 pthread_key_create
一次筹我,這時就要使用 pthread_once
與它配合。
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void))帆离;
第一個參數(shù) once_control
指向一個 pthread_once_t
對象蔬蕊,這個對象必須是常量 PTHREAD_ONCE_INIT
,否則 pthread_once
函數(shù)會出現(xiàn)不可預(yù)料的結(jié)果哥谷。
第二個參數(shù) init_routine
袁串,是調(diào)用的初始化函數(shù)概而,不能有參數(shù),不能有返回值囱修。
如果成功則返回0赎瑰,失敗返回非0值。
3. 鍵與線程數(shù)據(jù)關(guān)聯(lián)
創(chuàng)建完鍵后破镰,必須將其與線程數(shù)據(jù)關(guān)聯(lián)起來餐曼。關(guān)聯(lián)后也可以獲得某一鍵對應(yīng)的線程數(shù)據(jù)。關(guān)聯(lián)鍵和數(shù)據(jù)使用的函數(shù)為:
int pthread_setspecific(pthread_key_t *key, const void *value)鲜漩;
第一參數(shù) key
指向鍵源譬。
第二參數(shù) value
是欲關(guān)聯(lián)的數(shù)據(jù)。
函數(shù)成功則返回0孕似,失敗返回非0值踩娘。
注意:用 pthread_setspecific
為一個鍵指定新的線程數(shù)據(jù)時,并不會主動調(diào)用析構(gòu)函數(shù)釋放之前的內(nèi)存喉祭,所以調(diào)用線程必須自己釋放原有的線程數(shù)據(jù)以回收內(nèi)存养渴。
4. 獲取鍵管理的線程數(shù)據(jù)
獲取與某一個鍵關(guān)聯(lián)的數(shù)據(jù)使用函數(shù)的函數(shù)為:
void *pthread_getspecific(pthread_key_t *key);
參數(shù) key
指向鍵。
如果有與此鍵對應(yīng)的數(shù)據(jù)泛烙,則函數(shù)返回該數(shù)據(jù)理卑,否則返回NULL。
5. 刪除一個鍵
刪除一個鍵使用的函數(shù)為:
int pthread_key_delete(pthread_key_t key);
參數(shù) key
為要刪除的鍵蔽氨。
成功則返回0藐唠,失敗返回非0值。
注意:該函數(shù)將鍵設(shè)置為可用鹉究,以供下一次調(diào)用 pthread_key_create()
使用宇立。它并不檢查當(dāng)前是否有線程正在使用該鍵對應(yīng)的線程數(shù)據(jù),所以它并不會觸發(fā)函數(shù) pthread_key_create
中定義的 destructor
函數(shù)自赔,也就不會釋放該鍵關(guān)聯(lián)的線程數(shù)據(jù)所占用的內(nèi)存資源妈嘹,而且在將 key
設(shè)置為可用后,在線程退出時也不會再調(diào)用析構(gòu)函數(shù)匿级。所以在將 key 設(shè)置為可用之前蟋滴,必須要確定:
- 所有線程已經(jīng)析構(gòu) key 對應(yīng)的線程變量(要么顯示析構(gòu)染厅,要么線程退出);
- 不再使用該 key痘绎。 這兩個條件一般難以確定,所以實際使用時一般也不會將一個 key 設(shè)置為可用.
線程特有數(shù)據(jù)(TSD)的實現(xiàn)
在 Linux 中每個進程有一個全局的數(shù)組 __pthread_keys
肖粮,數(shù)組中存放著 稱為 key
的結(jié)構(gòu)體孤页,定義類似如下:
struct pthread_key_struct {
uintptr_t seq;
void (*destructor)(void*);
} __pthread_keys[PTHREAD_KEYS_MAX];
在 key
結(jié)構(gòu)中 seq
為一個序列號,用來作為使用標(biāo)志指示這個結(jié)構(gòu)在數(shù)組中是否正在使用涩馆,初始化時被設(shè)為0行施,即表示不在使用
允坚。destructor
用來存放一個析構(gòu)函數(shù)指針。
如果
destructor
不為空蛾号,在線程退出時稠项,將key
對應(yīng)的 TSD 作為參數(shù)調(diào)用析構(gòu)函數(shù),以釋放分配的緩存區(qū)鲜结。
pthread_create_key
會從數(shù)組中找到一個還未使用的 key
元素展运,將其序列號 seq
加1,并記錄析構(gòu)函數(shù)地址精刷,并將 key
在數(shù)組 __pthread_keys
中的下標(biāo)作為返回值返回拗胜。那么如何判斷一個 key
正在使用呢?
#define KEY_UNUSED(seq) (((seq) & 1) == 0)
如果 key
的序列號 seq 為偶數(shù)則表示未分配怒允,分配時將 seq 加1變成奇數(shù)埂软,即表示正在使用。這個操作過程采用原子 CAS 來完成纫事,以保證線程安全勘畔。在 pthread_key_delete()
時也將序列號 seq 加1,表示可以再被使用儿礼,通過序列號機制來保證回收的 key
不會被復(fù)用(復(fù)用 key
可能會導(dǎo)致線程在退出時可能會調(diào)用錯誤的析構(gòu)函數(shù))咖杂。但是一直加1會導(dǎo)致序列號回繞,還是會復(fù)用 key
蚊夫,所以調(diào)用 pthread_create_key
獲取可用的 key
時會檢查是否有回繞風(fēng)險诉字,如果有則創(chuàng)建失敗。
#define KEY_USABLE(seq) (((uintptr_t)(seq)) < ((uintptr_t) ((seq) + 2)))
除了進程范圍內(nèi)的 key
結(jié)構(gòu)數(shù)組外知纷,系統(tǒng)還在進程中維護關(guān)于每個線程的控制塊 TCB(用于管理寄存器壤圃,線程棧等),里面有一個 pthread_key_data
類型的數(shù)組琅轧。這個數(shù)組中的元素數(shù)量和進程中的 key
數(shù)組數(shù)量相等伍绳。pthread_key_data
的定義類似如下:
struct pthread_key_data {
uintptr_t seq;
void* data;
};
根據(jù) pthread_key_create()
返回的可用的 key
在 __pthread_keys
數(shù)組中的下標(biāo), pthread_setspecific()
在 pthread_key_data
的數(shù)組 中定位相同下標(biāo)的一個元素 pthread_key_data
乍桂,并設(shè)置其序號 seq 設(shè)置為對應(yīng)的 key
的序列號冲杀,數(shù)據(jù)指針 data
指向設(shè)置線程特有數(shù)據(jù)(TSD)的值。
pthread_getspecific()
用于將 pthread_setspecific()
設(shè)置的 data
取出睹酌。
線程退出時权谁,pthread_key_data
中的序號 seq 用于判斷該 key 是否仍在使用中(即與在 __pthread_keys
中的同一個下標(biāo)對應(yīng)的 key 的序列號 seq 是否相同),若是則將 pthread_key_data
中 data(即 線程特有數(shù)據(jù) TSD)作為參數(shù)調(diào)用析構(gòu)函數(shù)憋沿。
pthread_key_delete
函數(shù)會將 key 的序列號 seq 加1旺芽,變成奇數(shù)(奇數(shù)表示該 key 可用),可能會使 key 的 seq 和pthread_key_data
中的序號 seq 不相等,導(dǎo)致pthread_key_data
中的線程私有數(shù)據(jù)無法釋放采章。所有在通過pthread_key_delete
釋放一個 key 時运嗜,要保證線程特有數(shù)據(jù)已經(jīng)釋放。
由于系統(tǒng)在每個進程中 pthread_key_t
類型的數(shù)量是有限的悯舟,所有在進程中并不能獲取無限個 pthread_key_t
類型担租。Linux 中可以通過 PTHREAD_KEY_MAX(定義于 limits.h 文件中)或者系統(tǒng)調(diào)用 sysconf(_SC_THREAD_KEYS_MAX)
來確定當(dāng)前系統(tǒng)最多支持多少個 key
。 Linux 中默認是 1024 個 key抵怎,這對大多數(shù)程序來書已經(jīng)夠了翩活。如果一個線程中有多個線程局部存儲變量(TLS),通潮愎螅可以將這些變量封裝到一個數(shù)據(jù)結(jié)構(gòu)中菠镇,然后使用封裝后的數(shù)據(jù)結(jié)構(gòu)和一個線程局部變量相關(guān)聯(lián),這樣就能減少對鍵值的使用承璃。
參考
https://blog.csdn.net/hustraiet/article/details/9857919
https://blog.csdn.net/hustraiet/article/details/9857919
https://blog.csdn.net/caigen1988/article/details/7901248
http://www.bidutools.com/?p=2443
https://spockwangs.github.io/blog/2017/12/01/thread-local-storage/
http://www.reibang.com/p/71c2f80d7bd1
https://blog.csdn.net/cywosp/article/details/26469435
http://www.embeddedlinux.org.cn/emblinuxappdev/117.htm