再谈线程局部变量

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

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
#include
#include

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
#include

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 所指键值已经
 *    初始化,则返回调用线程对应此索引键值的线程局部变量
 * @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 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
#include

// 每个线程退出时回调此函数来释放线程局部变量的动态分配的内存
static void destructor(void *arg)
{
    acl_myfree(arg);
}

static void *thread_fn(void *arg)
{
    static 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://acl.sourceforge.net/ ),参考 acl_project/lib_acl/src/thread/, acl_project/lib_acl/include/thread/ 下的内容。

你可能感兴趣的:(再谈线程局部变量)