線程局部存儲(Thread Local Storage,TLS)主要用于在多線程中,存儲和維護一些線程相關的資料,存儲的資料會被關聯到目前線程中去,并不需要鎖來維護。。
是以也沒有多線程間資源競争問題,那如何去實作TLS存儲呢,主要有以下幾種方式:
- gcc和clang的
修飾符__thread
- windows下msvc的
__declspec(thread)
- pthread庫
和pthread_setspecific
接口pthread_getspecific
- windows下的
TlsSetValue
TlsGetValue
__thread和__declspec(thread)的使用
其中__thread和__declspec(thread)用起來最為友善,隻需要在static或者全局變量前加上此修飾符,然後線上程裡面通路變量就行了
例如:
tb_void_t tb_thread_func(tb_cpointer_t priv)
{
// 定義一個線程局部變量
static __thread int a = 0;
// 初始化這個變量,設定為目前線程id
if (!a) a = tb_thread_self();
}
如果運作多個線程的話,上述代碼中每個線程的變量a的值,都是不相同的,值為每個線程的id
而
__declspec(thread)
用起來也是類似,隻需替換下
__thread
就行了
雖然這兩個修飾符用起來很友善,但是需要編譯器支援,雖然現在大部分平台的編譯器都已支援,但是作為跨平台開發,這樣還是不夠的
畢竟還是有不少低版本gcc,不一定支援
__thread
,尤其是嵌入式開發領域,交叉編譯工具鍊中的編譯器支援力度差異還是蠻大的。。
另外,使用
__thread
進行tls資料維護,需要手動管理相關記憶體的釋放問題,用的不好很容易導緻記憶體洩露。。
pthread接口
pthread 的 tls 相關接口,比較完善,并且支援注冊free函數,線上程退出的時候,自動釋放相關tls資料,避免記憶體洩露,但是使用上稍顯複雜了些
我們看個簡單的例子:
// 測試線程中tls變量存儲的key,需定義為全局或者static
static pthread_key_t g_local_key = 0;
static tb_void_t tb_thread_local_free(tb_pointer_t priv)
{
tb_trace_i("thread[%lx]: free: %p", tb_thread_self(), priv);
}
static tb_void_t tb_thread_local_init(tb_void_t)
{
// 建立tls的key,并且設定自動釋放函數
pthread_key_create(&g_local_key, tb_thread_local_free);
}
static tb_int_t tb_thread_local_test(tb_cpointer_t priv)
{
// 在所有線程中,僅執行一次,用于線上程内部初始化 tls 的 key
static pthread_once_t s_once = PTHREAD_ONCE_INIT;
pthread_once(&s_once, tb_thread_local_init);
// 嘗試讀取目前tls資料
tb_size_t local;
if (!(local = (tb_size_t)pthread_getspecific(g_local_key)))
{
// 設定tls資料為目前線程id
tb_size_t self = tb_thread_self();
if (0 == pthread_setspecific(g_local_key, (tb_pointer_t)self))
local = self;
}
return 0;
}
看上去複雜了些,但是更加靈活,如果不需要線上程内部建立key的話,就不需要調用
pthread_once
了,直接把建立好的key傳入線程内部去通路就好。
TlsSetValue 接口
此套接口(TlsSetValue, TlsGetValue, TlsAlloc, TlsFree),屬于windows的tls操作接口,當然不能跨平台了,使用起來和pthread的差不多,但是無法注冊自動釋放函數,并且也沒提供類似
pthread_once
的接口
線上程内部自建立key,功能上稍顯不足。。
static tb_int_t tb_thread_local_test(tb_cpointer_t priv)
{
// 建立一個tls的key,注:此處非線程安全,最好放到類似pthread_once提供的init函數中去建立
// 此處就臨時先這麼寫了,僅僅隻是為了友善描述api用法,不要照搬哦。。
static DWORD s_key = 0;
if (!s_key) s_key = TlsAlloc();
// 嘗試讀取目前tls資料
DWORD local;
if (!(local = TlsGetValue(s_key)))
{
// 設定tls資料為目前線程id
tb_size_t self = tb_thread_self();
if (TlsSetValue(s_key, (LPVOID)self))
local = self;
}
return 0;
}
其實windows上還提供了FlsAlloc, FlsSetValue系列接口,給協程使用,并且支援注冊自動釋放的回調函數,不過對系統版本有些要求,像xp這些老系統就用不了了。。
這裡就不多描述了。
tbox提供的 thread_local
接口封裝
thread_local
最近對tbox的tls接口進行了改造,并且重構了實作邏輯,在剪口易用性、功能性以及效率上都得到了很大的提升。。
目前支援以下功能:
- 支援注冊自動釋放回調,保證線上程退出時,自動釋放設定的tls資料
- 支援線上程内部進行線程安全的key建立
- tbox退出時會自動銷毀所有建立的key,當然也可以提前主動銷毀它
用起來也很友善,很pthread很類似,但是内部自動調用了
pthread_once
,不用想pthread那樣顯式的去調用它了,例如:
static tb_void_t tb_demo_thread_local_free(tb_cpointer_t priv)
{
tb_trace_i("thread[%lx]: free: %p", tb_thread_self(), priv);
}
static tb_int_t tb_demo_thread_local_test(tb_cpointer_t priv)
{
/* 線程安全地初始化一個tls對象,相當于key,并且注冊自動free回調
*
* 注:雖然所有線程都會執行到這個tb_thread_local_init
* 但是s_local的tls對象,隻會確定初始化一次,内部有類似pthread_once接口來維護
*/
static tb_thread_local_t s_local = TB_THREAD_LOCAL_INIT;
if (!tb_thread_local_init(&s_local, tb_demo_thread_local_free)) return -1;
// 嘗試讀取目前tls資料
tb_size_t local;
if (!(local = (tb_size_t)tb_thread_local_get(&s_local)))
{
// 設定tls資料為目前線程id
tb_size_t self = tb_thread_self();
if (tb_thread_local_set(&s_local, (tb_cpointer_t)self))
local = self;
}
return 0;
}
線上程退出時,它會自動調用free回調,釋放對應殘留的tls資料,并且在
tb_exit
退出後,銷毀所有建立的tls對象
當然你可以可以主動調用:
tb_thread_local_exit(&s_local)
來銷毀它。。
tbox的這套接口,相比pthread減少了一個init的回調函數,比windows那套多了自動釋放的機制,并且同時支援跨平台。。
雜談
之前我看到一些庫中,對
__thread
和pthread接口進行了混用,感到很是莫名,個人感覺是有問題的,例如:
static __thread pthread_key_t g_key;
原本pthread文檔中就明确表述key需要全局或者static存儲,而這裡加上
__thread
後,其實每個線程通路的key都不是同一個key了哦。。
總結
如果僅僅隻是想用來線上程内部存儲一些簡單的int資料,并且不考慮完備的跨平台支援,那麼建議直接使用
__thread
或者
__declspec(thread)
j就行了,非常友善易用
如果要考慮跨平台操作的話,tbox的tls接口也是個不錯的選擇哦。。
個人首頁:
TBOOX開源工程原文出處:
http://tboox.org/cn/2016/09/28/thread-local/