[置頂] 再談線程局部變量

  在文章 多線程開發時線程局部變量的使用 中,曾詳細提到如何使用 __thread (Unix 平臺) 或 __declspec(thread) (win32 平臺)這類修飾符來申明定義和使用線程局部變量(當然在ACL庫裏統一了使用方法,將 __declspec(thread) 重定義爲 __thread),另外,爲了能夠正確釋放由 __thread 所修飾的線程局部變量動態分配的內存對象,ACL庫裏增加了個重要的函數:acl_pthread_atexit_add()/2,此函數主要作用是當線程退出時自動調用應用的釋放函數來釋放動態分配給線程局部變量的內存。以 __thread 結合 acl_pthread_atexit_add()/2 來使用線程局部變量非常簡便,但該方式卻存在以下主要的缺點(將 __thread/__declspec(thread) 類線程局部變量方式稱爲 “靜態 TLS 模型”):

  如果動態庫(.so 或 .dll)內部有以 __thread/__declspec(thread) 申明使用的線程局部變量,而該動態庫被應用程序動態加載(dlopen/LoadLibrary)時,如果使用這些局部變量會出現內存非法越界問題,原因是動態庫被可執行程序動態加載時此動態庫中的以“靜態TLS模型”定義的線程局部變量無法被系統正確地初始化(參見:Sun 的C/C++ 編程接口 及 MSDN 中有關 “靜態 TLS 模型 的使用注意事項)。

  爲解決 “靜態 TLS 模型 不能動態裝載的問題,可以使用 “動態 TLS 模型”來使用線程局部變量。下面簡要介紹一下 Posix 標準和 win32 平臺下 “動態 TLS 模型” 的使用:

  1、Posix 標準下 “動態 TLS 模型” 使用舉例:

 

#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>

static pthread_key_t key;

// 每個線程退出時回調此函數來釋放線程局部變量的動態分配的內存
static void destructor(void *arg)
{
    free(arg);
}

static init(void)
{
    // 生成進程空間內所有線程的線程局部變量所使用的鍵值
    pthread_key_create(&key, destructor);
}

static void *thread_fn(void *arg)
{
    char *ptr;

    // 獲得本線程對應 key 鍵值的線程局部變量
    ptr = pthread_getspecific(key);
    if (ptr == NULL) {
        // 如果爲空,則生成一個
        ptr = malloc(256);
        // 設置對應 key 鍵值的線程局部變量
        pthread_setspecific(key, ptr);
    }

     /* do something */

     return (NULL);
}

static void run(void)
{
     int   i, n = 10;  
     pthread_t tids[10];  

    // 創建新的線程
    for (i = 0; i < n; i++) {  
        pthread_create(&tids[i], NULL, thread_fn, NULL);  
    }  

    // 等待所有線程退出
    for (i = 0; i < n; i++) {  
        pthread_join(&tids[i], NULL);  
    }  
}

int main(int argc, char *argv[])
{
    init();
    run();
    return (0);
}

  可以看出,在同一進程內的各個線程使用同樣的線程局部變量的鍵值來“取得/設置”線程局部變量,所以在主線程中先初始化以獲得一個唯一的鍵值。如果不能在主線程初始化時獲得這個唯一鍵值怎麼辦? Posix 標準規定了另外一個函數:pthread_once(pthread_once_t *once_control, void (*init_routine)(void)), 這個函數可以保證 init_routine 函數在多線程內僅被調用一次,稍微修改以上例子如下:

#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>

static pthread_key_t key;

// 每個線程退出時回調此函數來釋放線程局部變量的動態分配的內存
static void destructor(void *arg)
{
    free(arg);
}

static init(void)
{
    // 生成進程空間內所有線程的線程局部變量所使用的鍵值
    pthread_key_create(&key, destructor);
}

static pthread_once_t once_control = PTHREAD_ONCE_INIT;

static void *thread_fn(void *arg)
{
    char *ptr;

    // 多個線程調用 pthread_once 後僅能是第一個線程纔會調用 init 初始化
    // 函數,其它線程雖然也調用 pthread_once 但並不會重複調用 init 函數,
    // 同時 pthread_once 保證 init 函數在完成前其它線程都阻塞在
    // pthread_once 調用上(這一點很重要,因爲它保證了初始化過程)
    pthread_once(&once_control, init);

    // 獲得本線程對應 key 鍵值的線程局部變量
    ptr = pthread_getspecific(key);
    if (ptr == NULL) {
        // 如果爲空,則生成一個
        ptr = malloc(256);
        // 設置對應 key 鍵值的線程局部變量
        pthread_setspecific(key, ptr);
    }
     
     /* do something */

     return (NULL);
}

static void run(void)
{
     int   i, n = 10;  
     pthread_t tids[10];  

    // 創建新的線程
    for (i = 0; i < n; i++) {  
        pthread_create(&tids[i], NULL, thread_fn, NULL);  
    }  

    // 等待所有線程退出
    for (i = 0; i < n; i++) {  
        pthread_join(&tids[i], NULL);  
    }  
}

int main(int argc, char *argv[])
{
    run();
    return (0);
}

  可見 Posix 標準當初做此類規定時是多麼的周全與謹慎,因爲最早期的 C 標準庫有很多函數都是線程不安全的,後來通過這些規定,使 C 標準庫的開發者可以“修補“這些函數爲線程安全類的函數。

 

  2、win32 平臺下 “動態 TLS 模型” 使用舉例:

static DWORD key;

static void init(void)
{
    // 生成線程局部變量的唯一鍵索引值
    key = TlsAlloc();
}

static DWORD WINAPI thread_fn(LPVOID data)
{
    char *ptr;

    ptr = (char*) TlsGetValue(key);  // 取得線程局部變量對象
    if (ptr == NULL) {
        ptr = (char*) malloc(256);
        TlsSetValue(key, ptr);  // 設置線程局部變量對象
    }

    /* do something */

    free(ptr);  // 應用自己需要記住釋放由線程局部變量分配的動態內存
    return (0);
}

static void run(void)
{
    int   i, n = 10;
    unsigned int tid[10];
    HANDLE handles[10];

    // 創建線程
    for (i = 0; i < n; i++) {
       handles[i] =  _beginthreadex(NULL,
                                  0,
                                  thread_fn,
                                  NULL,
                                  0,
                                  &tid[i]);
    }

    // 等待所有線程退出
    for (i = 0; i < n; i++) {
        WaitForSingleObject(handles[i]);
    }
}

int main(int argc, char *argv[])
{
    init();
    run();
    return (0);
}

 

    在 win32 下使用線程局部變量與 Posix 標準有些類似,但不幸的是線程局部變量所動態分配的內存需要自己記着去釋放,否則會造成內存泄露。另外還有一點區別是,在 win32 下沒有 pthread_once()/2 類似函數,所以我們無法直接在各個線程內部調用 TlsAlloc() 來獲取唯一鍵值。在ACL庫模擬實現了 pthread_once()/2 功能的函數,如下:

 

int acl_pthread_once(acl_pthread_once_t *once_control, void (*init_routine)(void))
{
	int   n = 0;

	if (once_control == NULL || init_routine == NULL) {
		acl_set_error(ACL_EINVAL);
		return (ACL_EINVAL);
	}

	/* 只有第一個調用 InterlockedCompareExchange 的線程纔會執行 init_routine,
	 * 後續線程永遠在 InterlockedCompareExchange 外運行,並且一直進入空循環
	 * 直至第一個線程執行 init_routine 完畢並且將 *once_control 重新賦值,
	 * 只有在多核環境中多個線程同時運行至此時纔有可能出現短暫的後續線程空循環
	 * 現象,如果多個線程順序至此,則因爲 *once_control 已經被第一個線程重新
	 * 賦值而不會進入循環體內
	 * 只所以如此處理,是爲了保證所有線程在調用 acl_pthread_once 返回前
	 * init_routine 必須被調用且僅能被調用一次
	 */
	while (*once_control != ACL_PTHREAD_ONCE_INIT + 2) {
		if (InterlockedCompareExchange(once_control,
			1, ACL_PTHREAD_ONCE_INIT) == ACL_PTHREAD_ONCE_INIT)
		{
			/* 只有第一個線程纔會至此 */
			init_routine();
			/* 將 *conce_control 重新賦值以使後續線程不進入 while 循環或
			 * 從 while 循環中跳出
			 */
			*once_control = ACL_PTHREAD_ONCE_INIT + 2;
			break;
		}
		/* 防止空循環過多地浪費CPU */
		if (++n % 100000 == 0)
			Sleep(10);
	}
	return (0);
}

 

  3、使用ACL庫編寫跨平臺的 “動態 TLS 模型” 使用舉例:

#include "lib_acl.h"
#include <stdlib.h>
#include <stdio.h>

static acl_pthread_key_t key = -1;

// 每個線程退出時回調此函數來釋放線程局部變量的動態分配的內存
static void destructor(void *arg)
{
    acl_myfree(arg);
}

static init(void)
{
    // 生成進程空間內所有線程的線程局部變量所使用的鍵值
    acl_pthread_key_create(&key, destructor);
}

static acl_pthread_once_t once_control = ACL_PTHREAD_ONCE_INIT;

static void *thread_fn(void *arg)
{
    char *ptr;

    // 多個線程調用 acl_pthread_once 後僅能是第一個線程纔會調用 init 初始化
    // 函數,其它線程雖然也調用 acl_pthread_once 但並不會重複調用 init 函數,
    // 同時 acl_pthread_once 保證 init 函數在完成前其它線程都阻塞在
    // acl_pthread_once 調用上(這一點很重要,因爲它保證了初始化過程)
    acl_pthread_once(&once_control, init);

    // 獲得本線程對應 key 鍵值的線程局部變量
    ptr = acl_pthread_getspecific(key);
    if (ptr == NULL) {
        // 如果爲空,則生成一個
        ptr = acl_mymalloc(256);
        // 設置對應 key 鍵值的線程局部變量
        acl_pthread_setspecific(key, ptr);
    }

     /* do something */

     return (NULL);
}

static void run(void)
{
     int   i, n = 10;  
     acl_pthread_t tids[10];  

    // 創建新的線程
    for (i = 0; i < n; i++) {  
        acl_pthread_create(&tids[i], NULL, thread_fn, NULL);  
    }  

    // 等待所有線程退出
    for (i = 0; i < n; i++) {  
        acl_pthread_join(&tids[i], NULL);  
    }  
}

int main(int argc, char *argv[])
{
    acl_init();  // 初始化 acl 庫
    run();
    return (0);
}

   這個例子是跨平臺的,它消除了UNIX、WIN32平臺之間的差異性,同時當我們在WIN32下開發多線程程序及使用線程局部變量時不必再那麼煩鎖了,但直接這麼用依然存在一個問題:因爲每創建一個線程局部變量就需要分配一個索引鍵,而每個進程內的索引鍵是有數量限制的(在LINUX下是1024,BSD下是256,在WIN32下也就是1000多),所以如果要以”TLS動態模型“創建線程局部變量還是要小心不可超過系統限制。ACL庫對這一限制做了擴展,理論上講用戶可以設定任意多個線程局部變量(取決於你的可用內存大小),下面主要介紹一下如何用ACL庫來打破索引鍵的系統限制來創建更多的線程局部變量。

  4、使用ACL庫創建線程局部變量

  接口介紹如下:

/**
 * 設置每個進程內線程局部變量的最大數量
 * @param max {int} 線程局部變量限制數量
 */
ACL_API int acl_pthread_tls_set_max(int max);

/**
 * 獲得當前進程內線程局部變量的最大數量限制
 * @return {int} 線程局部變量限制數量
 */
ACL_API int acl_pthread_tls_get_max(void);

/**
 * 獲得對應某個索引鍵的線程局部變量,如果該索引鍵未被初始化則初始之
 * @param key_ptr {acl_pthread_key_t} 索引鍵地址指針,如果是由第一
 *    個線程調用且該索引鍵還未被初始化(其值應爲 -1),則自動初始化該索引鍵
 *    並將鍵值賦予該指針地址,同時會返回NULL; 如果 key_ptr 所指鍵值已經
 *    初始化,則返回調用線程對應此索引鍵值的線程局部變量;爲了避免
 *    多個線程同時對該 key_ptr 進行初始化,建議將該變量聲明爲 __thread
 *    即線程安全的局部變量
 * @return {void*} 對應索引鍵值的線程局部變量
 */
ACL_API void *acl_pthread_tls_get(acl_pthread_key_t *key_ptr);

/**
 * 設置某個線程對應某索引鍵值的線程局部變量及自動釋放函數
 * @param key {acl_pthread_key_t} 索引鍵值,必須是 0 和
 *    acl_pthread_tls_get_max() 返回值之間的某個有效的數值,該值必須
 *    是由 acl_pthread_tls_get() 初始化獲得的
 * @param ptr {void*} 對應索引鍵值 key 的線程局部變量對象
 * @param free_fn {void (*)(void*)} 線程退出時用此回調函數來自動釋放
 *    該線程的線程局部變量 ptr 的內存對象
 * @return {int} 0: 成功; !0: 錯誤
 * @example:
 *    static void destructor(void *arg)
 *    {
 *        acl_myfree(arg};
 *    }
 *    static void test(void)
 *    {
 *        static __thread acl_pthread_key_t key = -1;
 *        char *ptr;
 *
 *        ptr = acl_pthread_tls_get(&key);
 *        if (ptr == NULL) {
 *            ptr = (char*) acl_mymalloc(256);
 *            acl_pthread_tls_set(key, ptr, destructor);
 *        }
 *    }
 */
ACL_API int acl_pthread_tls_set(acl_pthread_key_t key, void *ptr, void (*free_fn)(void *));

 

  現在使用ACL庫中的這些新的接口函數來重寫上面的例子如下:

#include "lib_acl.h"
#include <stdlib.h>
#include <stdio.h>

// 每個線程退出時回調此函數來釋放線程局部變量的動態分配的內存
static void destructor(void *arg)
{
    acl_myfree(arg);
}

static void *thread_fn(void *arg)
{
    // 該 key 必須是線程局部安全的
    static __thread acl_pthread_key_t key = -1;
    char *ptr;

    // 獲得本線程對應 key 鍵值的線程局部變量
    ptr = acl_pthread_tls_get(&key);
    if (ptr == NULL) {
        // 如果爲空,則生成一個
        ptr = acl_mymalloc(256);
        // 設置對應 key 鍵值的線程局部變量
        acl_pthread_tls_set(key, ptr, destructor);
    }

    /* do something */

    return (NULL);
}

static void run(void)
{
     int   i, n = 10;  
     acl_pthread_t tids[10];  

    // 創建新的線程
    for (i = 0; i < n; i++) {  
        acl_pthread_create(&tids[i], NULL, thread_fn, NULL);  
    }  

    // 等待所有線程退出
    for (i = 0; i < n; i++) {  
        acl_pthread_join(&tids[i], NULL);  
    }  
}

int main(int argc, char *argv[])
{
    acl_init();  // 初始化ACL庫
    // 打印當前可用的線程局部變量索引鍵的個數
    printf(">>>current tls max: %d\n", acl_pthread_tls_get_max());
    // 設置可用的線程局部變量索引鍵的限制個數
    acl_pthread_tls_set_max(10240);

    run();
    return (0);
}

 

  這個例子似乎又比前面的例子更加簡單靈活,如果您比較關心ACL裏的內部實現,請直接下載ACL庫源碼(http://sourceforge.net/projects/acl/ ),參考 acl_project/lib_acl/src/thread/, acl_project/lib_acl/include/thread/ 下的內容。

 

下載:http://sourceforge.net/projects/acl/

svn:svn checkout svn://svn.code.sf.net/p/acl/code/trunk acl-code

github:https://github.com/zhengshuxin/acl

 

個人微博:http://weibo.com/zsxxsz

 bbs:http://www.aclfans.com

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章