linux编程-CC++每线程(thread-local)变量的使⽤
在⼀个进程中定义的全局或静态变量都是所有线程可见的,即每个线程共同操作⼀块存储区域。⽽有时我们可能有这样的需求:对于⼀个全局变量,每个线程对其的修改只在本线程内有效,各线程之间互不⼲扰。即每个线程虽然共享这个全局变量的名字,但这个变量的值就像只有在本线程内才会被修改和读取⼀样。
线程局部存储和线程特有数据都可以实现上述需求。
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);
sleep(4);
/* 获得key所关联的value */linux下的sleep函数
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 [OL]. Lawrence Crowl, 2008-06-11.
[3] C++ ISO drafts
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论