`
zsxxsz
  • 浏览: 444953 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

再谈线程局部变量

阅读更多

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

国内镜像:http://git.oschina.net/acl-dev/acl/tree/master
个人微博:http://weibo.com/zsxxsz

QQ 群:242722074

8
0
分享到:
评论
1 楼 lgqss 2016-03-08  
最后的例子用了__thread,依然不能用在动态库中?

相关推荐

    21天学通Java-由浅入深

    60分钟) 217 11.1 异常处理基本介绍 217 11.1.1 try和catch捕获异常 217 11.1.2 try-catch语句使用注意点 218 11.1.3 finally语句的使用 220 11.1.4 再谈异常处理注意点 222 11.2 异常的分类 223 11.2.1 捕获异常 ...

    javaSE代码实例

    15.2.2 局部变量与局部内部类 329 15.2.3 静态方法中的局部内部类 331 15.3 静态内部类 332 15.3.1 语法规则 332 15.3.2 创建静态内部类的对象 332 15.3.3 静态/非静态内部类的区别 333 15.4 匿名内部...

    JAVA入门1.2.3:一个老鸟的JAVA学习心得 PART1(共3个)

    7.8.1 发现问题:当实例变量和局部变量重名 177 7.8.2 经常深藏不露的this关键字 178 7.8.3 在方法中调用方法 179 7.9 构造方法(Constructor) 181 7.9.1 构造(Constructor)方法初探 181 7.9.2 如何使用构造...

    Java入门1·2·3:一个老鸟的Java学习心得.PART3(共3个)

    7.8.1 发现问题:当实例变量和局部变量重名 177 7.8.2 经常深藏不露的this关键字 178 7.8.3 在方法中调用方法 179 7.9 构造方法(Constructor) 181 7.9.1 构造(Constructor)方法初探 181 7.9.2 如何使用构造...

    ASP.NET编程之道.part1.rar

    细节37 使用var创建隐型局部变量 细节38 Lambda表达式 细节39 生成缩略图的方法 细节40 使用缓冲流 …… 第6章 必知必会的28个ADO.NET、XML及LINQ方面的技术细节 第7章 必知必会的22个Web方面的技术细节 第4篇 开发...

    深入理解_Java_虚拟机 JVM_高级特性与最佳实践

    189 7.4.1 类与类加载器 / 189 7.4.2 双亲委派模型 / 191 7.4.3 破坏双亲委派模型 / 194 7.5 本章小结 / 197 第8章 虚拟机字节码执行引擎 / 198 8.1 概述 / 198 8.2 运行时栈帧结构 / 199 8.2.1 局部变量表 ...

    Java虚拟机

    8.2.1 局部变量表 8.2.2 操作数栈 8.2.3 动态连接 8.2.4 方法返回地址 8.2.5 附加信息 8.3 方法调用 8.3.1 解析 8.3.2 分派 8.3.3 动态类型语言支持 8.4 基于栈的字节码解释执行引擎 8.4.1 解释执行 ...

    .NET之美:.NET关键技术深入分析

    5.1.2 隐式类型的局部变量 5.1.3 匿名类型 5.1.4扩展方法 5.1.5 匿名方法和Lambda表达式 5.2集合 5.2.1理解集合 5.2.2创建集合类 5.2.3实现IEnumerable接口 5.3 LINQ查询 5.3.1 LINQ to Objects 5.3.2查询...

    Python核心编程第二版(ok)

     11.8.1 全局变量与局部变量   11.8.2 globa语句   11.8.3 作用域的数字   11.8.4 闭包   11.8.5 作用域和lambda   11.8.6 变量作用域和名称空间   11.9 递归   11.10 生成器   11.10.1 ...

    Python核心编程第二版

     11.8.1 全局变量与局部变量   11.8.2 globa语句   11.8.3 作用域的数字   11.8.4 闭包   11.8.5 作用域和lambda   11.8.6 变量作用域和名称空间   11.9 *递归   11.10 生成器   11.10.1 ...

    c#学习笔记.txt

    const指定无法修改字段或局部变量的值。 event声明一个事件。 extern指示外部实现此方法。 override提供从基类继承的虚拟成员的新实现。 readonly声明一个字段,该字段只能赋值为该声明的一部分或者在同一类的构造...

    C#微软培训资料

    第五章 变量和常量 .44 5.1 变 量 .44 5.2 常 量 .46 5.3 小 结 .47 第六章 类 型 转 换 .48 6.1 隐式类型转换 .48 6.2 显式类型转换 .53 6.3 小 结 .56 第七章 表 达 式 .58 7.1 操 作 符 .58 ...

Global site tag (gtag.js) - Google Analytics