再谈线程局部变量

  在文章 多线程开发时线程局部变量的使用 中,曾详细提到如何使用 __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

相关文章
相关标签/搜索