再談線程局部變量

  在文章 多線程開發時線程局部變量的使用 中,曾詳細提到如何使用 __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 模型」):git

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

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

  一、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 標準庫的開發者能夠「修補「這些函數爲線程安全類的函數。app

 

  二、win32 平臺下 「動態 TLS 模型」 使用舉例:svn

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);
}

 

  三、使用ACL庫編寫跨平臺的 「動態 TLS 模型」 使用舉例:spa

#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庫來打破索引鍵的系統限制來建立更多的線程局部變量。.net

  四、使用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

相關文章
相關標籤/搜索