线程私有数据
在单线程程序中,函数经常使用全局变量或静态变量,这是不会影响程序的正确性的,但如果线程调用的函数使用全局变量或静态变量,则很可能引起错误。因为这些函数使用的全局变量和静态变量无法为不同的线程保存各自的值,而当同一进程内的不同线程几乎同时调用这样的函数时就可能会有问题发生。
而解决这一问题的一种方式就是使用线程私有数据。线程私有数据采用了一种被称为一键多值的技术,即一个键对应多个数值。访问数据时都是通过键值来访问,好像是对一个变量进行访问,其实是在访问不同的数据。使用线程私有数据时,首先要为每个线程数据创建一个相关联的键。在各个线程内部,都使用这个公用的键来指代线程数据,但是在不同的线程中,这个键代表的数据是不同的。
这和JAVA中的ThreadLocal变量的思想有点像:尽管是一个变量,但是在不同的线程中调用就是取出的不同的值。
设计线程私有数据的原因:
- 在维护每个线程的私有数据的时候,我们可能会想到分配一个保存线程数据的数组,用线程的ID作为数组的索引来实现访问,但是有一个问题是:系统生成的线程ID不能保证是一个小而连续的整数, 并且用数组实现的时候由于其他线程也可以访问其数组中的数据,这样会引起数据的不安全;
- 线程私有数据提供了让基于进程的接口适应多线程环境的机制。
一个很明显的实例就是errno,以前的接口(线程出现以前)把errno定义为进程上下文中全局可访问的整数。系统调用和库例程在调用或执行失败时设置 errno,把它作为操作失败的附属结果。为了让线程也能够使用那些原本基于进程的系统调用和库例程,errno 被重定义为线程私有数据。这样,一个线程做了重置errno的操作也不会影响进程中其他线程的errno值。
线程私有变量函数
线程私有变量的相关函数为:
线程私有变量的初始化
int pthread_key_create (pthread_key_t *key, void (*destructor)(void *));
函数说明:key参数为指向一个键值的指针,第二个参数指明了一个destructor函数,如果这个参数不为空,那么当每个线程结束时,系统将调用这个函数来释放绑定在这个键上的内存块。
销毁线程私有变量
int pthread_key_delete (pthread_key_t key);
函数说明:key参数为一个键值。
向线程私有变量赋值
int pthread_setspecific (pthread_key_t key, const void *value);
函数说明:value参数是不同的线程中,key值所关联的私有数据地址,这些地址可以用malloc来分配。
获取线程私有变量
void *pthread_getspecific (pthread_key_t key);
函数说明:key参数为一个键值,返回值是线程私有变量的数据地址。
指定函数只执行一次
int phread_once(pthread_once_t *onceptr, void(*init)(void));
函数说明:本函数使用初值为PTHREAD_ONCE_INIT的once_control变量保证init_routine()函数在本进程执行序列中仅执行一次。注意的是,onceptr必须是一个非本地变量(即全局变量或者静态变量),而且必须初始化为PTHREAD_ONCE_INIT。
一般使用环境:在多线程环境中,有些事仅需要执行一次。通常当初始化应用程序时,可以比较容易地将其放在main函数中。但当你写一个库时,就不能在main里面初始化了,你可以用静态初始化,但使用一次初始化(pthread_once)会比较容易些。
例子:
pthread_key_t key; pthread_once_t r1_once = PTHREAD_ONCE_INIT; void destructor(void *ptr){ free(ptr); } void excute_once(void) { pthread_key_create(&key, destructor); } int main(){ pthread_once(&r1_once, excute_once); }
TSD池
作为TSD池,就是线程私有数据(Thread-specific Data)池。
当我们调用pthread_key_create()时,内核从Linux的TSD池中分配一项,将其值赋给key供以后访问使用,它的第一个参数key为指向键值的指针,第二个参数为一个函数指针,如果指针不为空,则在线程退出时将以key所关联的数据为参数调用destr_function(),释放分配的缓冲区。 key一旦被创建,所有线程都可以访问它,但各线程可以根据自己的需要往key中填入不同的值,这就相当于提供了一个同名而不同值的全局变量,一键多值。其中TSD池的结构如下:
内核支持的pthread_keys是有限的(一般是128),除了进程范围内的keys结构数组之外,系统还在进程内维护了关于多个线程的多条信息。这些特定于线程的信息我们称之为Pthread结构。其中部分内容是我们称之为pkey数组的一个128个元素的指针数组。系统维护的关于每个线程的信息结构图如下:
一旦我们在某个线程里面调用pthread_setspecific()将key与某个空间(这个空间可以是malloc申请的)相关连的时候,上面的结构就变成:
实际例子
#include<stdio.h> #include<unistd.h> #include<pthread.h> pthread_key_t key; void echomsg( void* param ) { printf( "destructor excuted in thread %lu, param = %lu\n", pthread_self(), *((unsigned long int*)param) ); } void* child1( void* param ) { unsigned long int tid = pthread_self(); printf( "thread %lu enter\n", tid ); pthread_setspecific( key, ( void* )tid ); sleep( 2 ); unsigned long int val = *((unsigned long int *)pthread_getspecific( key )); printf( "thread %lu returns %lu\n", tid, val ); sleep( 5 ); return ( void* )0; } void* child2( void* param ) { unsigned long int tid = pthread_self(); printf( "thread %lu enter\n", tid ); pthread_setspecific( key, ( void* )tid ); sleep( 1 ); unsigned long int val = *( (unsigned long int*)pthread_getspecific( key ) ); printf( "thread %lu returns %lu\n", tid, val ); sleep( 5 ); return ( void* )0; } int main() { pthread_t tid1, tid2; printf( "main thread enter\n" ); pthread_key_create( &key, echomsg ); pthread_create( &tid1, NULL, child1, NULL ); pthread_create( &tid2, NULL, child2, NULL ); sleep( 10 ); pthread_key_delete( key ); printf( "main thread exit\n" ); return 0; }
通过g++编译命令:
g++ -lpthread -o TSD.out TSD.cpp
最终的运行结果为:
kennie@cbib:~/pthreadDIR$ ./TSD.out main thread enter thread 3075607408 enter thread 3067214704 enter thread 3067214704 returns 3067214704 thread 3075607408 returns 3075607408 destructor excuted in thread 3067214704, param = 3067214704 destructor excuted in thread 3075607408, param = 3075607408 main thread exit