天天看點

線程局部存儲tls的使用

線程局部存儲(Thread Local Storage,TLS)主要用于在多線程中,存儲和維護一些線程相關的資料,存儲的資料會被關聯到目前線程中去,并不需要鎖來維護。。

是以也沒有多線程間資源競争問題,那如何去實作TLS存儲呢,主要有以下幾種方式:

  1. gcc和clang的

    __thread

    修飾符
  2. windows下msvc的

    __declspec(thread)

  3. pthread庫

    pthread_setspecific

    pthread_getspecific

    接口
  4. 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

接口封裝

最近對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/

繼續閱讀