`
zsxxsz
  • 浏览: 452087 次
  • 性别: 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标准定义了一些线程安全的函数,...

    浅谈计算机操作系统中线程与超线程技术及应用.pdf

    线程是操作系统中的基本执行单元,它是程序的一部分,具有独立的执行路径和局部变量。一个进程中可以包含多个线程,它们共享同一内存空间,可以并发执行,提高了处理器的使用率。线程间的通信和同步相对进程而言更为...

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

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

    多线程面试59题(含答案)_.zip

    最后,Java内存模型(JMM)和线程局部变量(ThreadLocal)也是多线程面试的重要组成部分。JMM规定了线程如何访问共享内存,确保并发环境下的正确性;ThreadLocal则为每个线程提供独立的变量副本,避免了线程间的数据冲突...

    简单的多线程例子

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

    浅谈java中的几种随机数

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

    浅谈Java中的几种随机数

    这个类在内部使用了线程局部变量来避免多线程之间的竞争,相比于使用Random类,它在生成大量随机数时可以提供更好的性能。在并发环境下,我们可以通过调用ThreadLocalRandom.current()来获取当前线程的随机数生成器...

    JVM面试题.pdf

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

    java jvm调优浅谈

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

    经典之谈——Java内存分配

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

    细谈VC程序调试的若干方法.doc

    - **Locals窗口**:显示当前作用域内的局部变量及其值。 - **Watch窗口**:手动添加需要监控的变量,实时查看其变化。 **2.4 使用输出窗口** - **Output窗口**:显示程序输出的信息,包括标准输出和调试输出。 - **...

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

    由一个简单的程序谈起――之二

    - **局部变量**:应尽可能将变量声明在最接近其使用位置的地方,以限制其作用域,这有助于降低代码复杂度和提高可读性。 - **成员变量**:对于需要在多个方法间共享或持久存储的数据,应当声明为类的成员变量。 - **...

Global site tag (gtag.js) - Google Analytics