`
zsxxsz
  • 浏览: 446266 次
  • 性别: 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,依然不能用在动态库中?

相关推荐

    浅谈Linux下的多线程编程.pdf

    8. **线程局部存储**: - 通过`pthread_getspecific`和`pthread_setspecific`函数,可以为每个线程创建独立的数据存储,避免线程间的数据混淆。 9. **线程安全的函数**: - POSIX标准定义了一些线程安全的函数,...

    浅谈linux多线程编程和windows多线程编程的异同.doc

    在高级特性上,如线程局部存储(TLS),Linux使用`pthread_getspecific()`和`pthread_setspecific()`,而Windows有`TlsAlloc()`, `TlsSetValue()`, 和`TlsGetValue()`。 在错误处理方面,Linux的错误通常返回错误码...

    简单的多线程例子

    线程局部存储(Thread Local Storage, TLS)是一种解决资源隔离的方法,每个线程都有自己的变量副本,避免了共享数据的冲突。 总结来说,“简单的多线程例子”通常涉及创建线程对象,定义线程的执行逻辑,以及正确...

    浅谈java中的几种随机数

    这个类是Java 7引入的,结合了单例模式和线程局部变量的优势。 值得注意的是,使用`Math.random()`和简单的取模操作`Math.abs(rnd.nextInt()) % n`来生成[0, n)范围内的随机数,可能会导致分布不均匀。正确的做法是...

    JVM面试题.pdf

    * 栈区:栈的结构是栈帧组成的,调用一个方法就压入一帧,帧上面存储局部变量表、操作数栈、方法出口等信息,局部变量表存放的是 8 大基础类型加上一个应用类型,所以还是一个指向地址的指针。 * 方法区:主要是存储...

    java jvm调优浅谈

    栈因为是运行单位,因此里面存储的信息都是跟当前线程(或程序)相关的信息,包括局部变量、程序运行状态、方法返回值等等,而堆只负责存储对象信息。 堆和栈的分离有很多优势,例如使得处理逻辑更为清晰,允许多个...

    经典之谈——Java内存分配

    2. **虚拟机栈**:每个线程都有一个与之对应的虚拟机栈,用于存储局部变量表、操作数栈、动态链接和方法出口等信息。每当执行一个方法,就会创建一个栈帧,用于存放方法的相关数据,方法执行完毕后,栈帧随之销毁。 ...

    浅谈jvm原理

    方法栈是 Java 方法执行的内存模型,每个方法在执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。PC 寄存器用于存储待执行指令的地址,以便线程恢复和跳转。Java 堆是 JVM 用于存放...

    浅谈Java引用和Threadlocal的那些事

    Threadlocal是一个线程局部变量,可以记录线程的状态。Threadlocal的key可以是虚引用,在Threadlocal.get()时,可能会发生GC,导致key为null。 Java中的引用类型和Threadlocal的使用都是Java开发中非常重要的知识点...

    浅谈Java中随机数的几种实现方式

    为优化性能,可以考虑将`Random`对象静态化,或者使用线程局部变量(`ThreadLocal`)来保存每个线程自己的`Random`实例。 在多线程环境中,`java.util.Random`类是线程安全的,这意味着多个线程可以安全地共享同一...

    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 捕获异常 ...

    ppk谈JavaScript.part06.rar

    全局作用域在整个脚本中都是可见的,而函数内部定义的变量和函数形成了局部作用域。了解作用域对于避免变量冲突和优化代码性能至关重要。 2. **闭包(Closures)**:闭包是一种特殊的函数,它可以访问并操作其自身...

    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 匿名内部...

    ppk谈JavaScript.part05.rar

    2. **异步编程**:JavaScript是单线程语言,但通过事件循环和回调函数、Promise、async/await等机制来处理异步任务,避免阻塞UI。 3. **错误处理**:异常处理是程序中必不可少的部分,JavaScript通过try...catch...

    浅谈Java内存区域划分和内存分配策略

    每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法出入口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。 本地方法栈 本地方法栈...

    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 如何使用构造...

    浅谈Java内存区域与对象创建过程

    2. 虚拟机栈(线程私有):虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的同时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成的过程,就对应着一...

    浅谈Java的虚拟机结构以及虚拟机内存的优化

    Java栈是另一个线程私有的区域,每个线程在执行方法时会创建一个栈帧,存储局部变量、操作数栈和返回地址。当方法调用结束,栈帧会被弹出,释放其所占的内存。 最后,本地方法栈与Java栈类似,但服务于JNI(Java ...

Global site tag (gtag.js) - Google Analytics