linux编程 – C/C++每线程(thread-local)变量的使用

  • Post author:
  • Post category:linux


在一个进程中定义的全局或静态变量都是所有线程可见的,即每个线程共同操作一块存储区域。而有时我们可能有这样的需求:对于一个全局变量,每个线程对其的修改只在本线程内有效,各线程之间互不干扰。即每个线程虽然共享这个全局变量的名字,但这个变量的值就像只有在本线程内才会被修改和读取一样。

线程局部存储和线程特有数据都可以实现上述需求。

1. 线程局部存储

线程局部存储提供了持久的每线程存储,每个线程都拥有一份对变量的拷贝。线程局部存储中的变量将一直存在,直到线程终止,届时会自动释放这一存储。一个典型的例子就是errno的定义(uClibc-0.9.32),每个线程都有自己的一份errno的拷贝,防止了一个线程获取errno时被其他线程干扰。

要定义一个线程局部变量很简单,只需简单的在全局或静态变量的声明中包含


__thread


说明符即可。例如:

static __thread int buf[MAX_ERROR_LEN];

这样定义的变量,在一个线程中只能看到本线程对其的修改。

关于线程局部变量的声明和使用,需要注意以下几点:

1. 如果变量声明中使用了关键字static或extern,那么关键字__thread必须紧随其后。

2. 与一般的全局或静态变量声明一样,线程局部变量在声明时可以设置一个初始值。

3. 可以使用C语言取址操作符(&)来获取线程局部变量的地址。

在一个线程中修改另一个线程的局部变量:

__thread变量并不是在线程之间完全隐藏的,每个线程保存自己的一份拷贝,因此每个线程的这个变量的地址不同。但这个地址是整个进程可见的,因此一个线程获得另外一个线程的局部变量的地址,就可以修改另一个线程的这个局部变量。

C++中对__thread变量的使用有额外的限制:

1. 在C++中,如果要在定义一个thread-local变量的时候做初始化,初始化的值必须是一个常量表达式。

2. __thread只能修饰POD类型,即不带自定义的构造、拷贝、赋值、析构的类型,不能有non-static的protected或private成员,没有基类和虚函数,因此对定义class做了很多限制。但可以改为修饰class指针类型便无需考虑此限制。

2. 线程特有数据

上面是C/C++语言实现每线程变量的方式,而POSIX thread使用getthreadspecific和setthreadspecific 组件来实现这一特性,因此编译要加-pthread,但是使用这种方式使用起来很繁琐,并且效率很低。不过我也简单讲一下用法。

使用线程特有数据需要下面几步:

1. 创建一个键(key),,用以将不同的线程特有数据区分开来。调用函数

pthread_key_create()

可创建一个key,且只需要在首个调用该函数的线程中创建一次。

2. 在不同线程中,使用

pthread_setspecific()

函数将这个key和本线程(调用者线程)中的某个变量的值关联起来,这样就可以做到不同线程使用相同的key保存不同的value。

3. 在各线程可通过

pthread_getspecific()

函数来取得本线程中key对应的值。

三个接口函数的说明:

#include <pthread.h>

int pthread_key_create(pthread_key_t * key, void (*destructor)(void *));

用于创建一个key,成功返回0。

函数destructor指向一个自定义函数,定义如下。在线程终止时,会自动执行该函数进行一些析构动作,例如释放与key绑定的存储空间的资源。如果无需解构,可将destructor置为NULL。

void dest(void *value)

{


/* Release storage pointed to by ‘value’ */

}

参数value是与key关联的指向线程特有数据块的指针。

注意,如果一个线程有多个线程特有数据块,那么对各个解构函数的调用顺序是不确定的,因此每个解构函数的设计要相互独立。

int pthread_setspecific(pthread_key_t key, const void * value);

用于设置key与本线程内某个指针或某个值的关联。成功返回0。

void *pthread_getspecific(pthread_key_t key);

用于获取key关联的值,由该函数的返回值的指针指向。如果key在该线程中尚未被关联,该函数返回NULL。

int pthread_key_delete(pthread_key_t key);

用于注销一个key,以供下一次调用pthread_key_create()使用。

Linux支持最多1024个key,一般是128个,所以通常key是够用的,如果一个函数需要多个线程特有数据的值,可以将它们封装为一个结构体,然后仅与一个key关联。

写一个例子:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

pthread_key_t key;

static void key_destrutor(void * value)
{
 printf("dest called\n");
 /* 这个例子中,关联key的值并没有malloc等操作,因此不用做释放动作。 */
 return (void)0;
}

int get_pspec_value_int()
{
 int * pvalue;
 pvalue = (int *)pthread_getspecific( key );
 return *pvalue;
}

void * thread_handler(void * args)
{
        int index = *((int *)args);
  int pspec;
  
  pspec = 0;
  /* 设置key与value的关联 */
  pthread_setspecific(key, (void *)&pspec);

        while (1)
        {
   sleep(4);
   /* 获得key所关联的value */
   pspec = get_pspec_value_int();
   printf("thread index %d = %d\n", index, pspec);
   /* 修改value的值,本例中用于测试不同线程的value不会互相干扰。 */
   pspec += index;
   pthread_setspecific(key, (void *)&pspec);
        }
        return (void *)0;
}

int main ()
{
        pthread_t pid1;
        pthread_t pid2;
        int ret;
        int index1 = 1, index2 = 2;
  
  struct thr1_st m_thr_v, *p_mthr_v;
  
  /* 创建一个key */
  pthread_key_create(&key, key_destrutor); 

        if (0 != (ret = pthread_create(&pid1, NULL, thread_handler, (void *)&index1)))
        {
   perror("create thread failed:");
   return 1;
        }

        if (0 != (ret = pthread_create(&pid2, NULL, thread_handler, (void *)&index2)))
  {
   perror("create thread failed:");
   return 1;
  }
  
  /* 设置key与value的关联 */
  memset(&m_thr_v, 0, sizeof(struct thr1_st));
  pthread_setspecific(key, (void *)&m_thr_v);

        while (1)
        {
                sleep(3);
    
    /* 获得key所关联的value */
    p_mthr_v = (struct thr1_st *)pthread_getspecific(key);
    printf("main len = %d\n", p_mthr_v->len);
    /* 修改value的值,本例中用于测试不同线程的value不会互相干扰。 */
    p_mthr_v->len += 5;
    pthread_setspecific(key, (void *)p_mthr_v);
        }
  
  /* 注销一个key */
  pthread_key_delete(key);
  pthread_join(pid1, 0);
  pthread_join(pid2, 0);

        return 0;
}

上面的例子说明了如何定义线程特有数据。其中由于本例中的数据只是一个value而已,所以并没有必须注册解构函数,而如果是进行了malloc的指针,则需要在解构函数中释放,否则会出现内存泄露。执行这个程序就会看到每个线程对关联到key的值的修改是互不干扰的,也即实现了线程特有数据存储。

另外值得注意的是,pthread_key_create()只需在第一个使用这个key的线程中调用一次即可,在这个例子中,很明显要在main函数中调用。而如果我们要实现一个库函数,这个库函数中需要创建并使用key,那么就会造成多次调用pthread_key_create()。



pthread_once()


函数可以解决这样的问题,其声明如下:

#include <pthread.h>
int pthread_once(pthread_once_t *once_control, void (*init)(void));

该函数可以做到无论有多少个线程对该函数调用了多少次,都只会在第一次被调用时执行自定义的init函数。

参数once_control是一个指针,指向初始化为PTHREAD_ONCE_INIT的静态变量,例如:

pthread_once_t once_var = PTHREAD_ONCE_INIT;

该变量通过自身状态的变化来控制只有在第一次被调用的时候才执行init回调函数。

参考资料;

[1] 孙剑等 译 Michael Kerrisk 著. Linux/UNIX系统编程手册(上) [M]. 人民邮电出版社,2014.

[2] Thread-Local Storage

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2659.htm

[OL]. Lawrence Crowl, 2008-06-11.

[3] C++ ISO drafts

http://www.csci.csusb.edu/dick/c++std/cd2/expr.html



版权声明:本文为jasonchen_gbd原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。