文档章节

再谈线程局部变量

郑树新
 郑树新
发布于 2014/09/03 12:11
字数 3032
阅读 17
收藏 0

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

 

个人微博:http://weibo.com/zsxxsz

 bbs:http://www.aclfans.com

© 著作权归作者所有

郑树新

郑树新

粉丝 104
博文 87
码字总数 161171
作品 2
昌平
程序员
私信 提问
JAVA中ThreadLocal用法介绍

概述 看名字好像是一个Thread的实现,其实并不是这样的,ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物...

晨曦艾伯特
2018/01/11
0
0
[虚拟机字节码执行引擎] 1 - 栈桢

本文主要内容 前言 运行时栈桢结构 已经学习了虚拟机内存区域、Class文件结构、类加载机制等知识,是时候学习虚拟机字节码执行过程了。 前言 虚拟机是一个相对物理机而言的概念,它们都有代码...

某昆
2018/01/06
0
0
java ThreadLocal

JDKAPI 解释: 该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其get或set方法)的每个线程都有自己的局部变量,它独立于初始化变量的副本...

smallsun512
2013/06/26
0
0
深入理解Python中的ThreadLocal变量(上)

我们知道多线程环境下,每一个线程均可以使用所属进程的全局变量。如果一个线程对全局变量进行了修改,将会影响到其他所有的线程。为了避免多个线程同时对变量进行修改,引入了线程同步机制,...

selfboot
2016/08/22
0
0
Linux中的线程局部存储(一)

在 Linux系统中使用C/C++进行多线程编程时,我们遇到最多的就是对同一变量的多线程读写问题,大多情况下遇到这类问题都是通过锁机制来处理,但这对 程序的性能带来了很大的影响,当然对于那些...

follitude
2016/06/17
16
0

没有更多内容

加载失败,请刷新页面

加载更多

Android双向绑定原理简述

Android双向绑定原理简述 双向绑定涉及两个部分,即将业务状态的变化传递给UI,以及将用户输入信息传递给业务模型。 首先我们来看业务状态是如何传递给UI的。开启dataBinding后,编译器为布局...

tommwq
今天
4
0
Spring系列教程八: Spring实现事务的两种方式

一、 Spring事务概念: 事务是一系列的动作,它们综合在一起才是一个完整的工作单元,这些动作必须全部完成,如果有一个失败的话,那么事务就会回滚到最开始的状态,仿佛什么都没发生过一样。...

我叫小糖主
今天
8
0
CentOS 的基本使用

1. 使用 sudo 命令, 可以以 root 身份执行命令, 必须要在 /etc/sudoers 中定义普通用户 2. 设置 阿里云 yum 镜像, 参考 https://opsx.alibaba.com/mirror # 备份mv /etc/yum.repos.d/CentO...

北漂的我
昨天
4
0
Proxmox VE技巧 移除PVE “没有有效订阅” 的弹窗提示

登陆的时候提示没有有效的订阅You do not have a valid subscription for this server. Please visit www.proxmox.com to get a list of available options. 用的是免费版的,所以每次都提示......

以谁为师
昨天
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部