私有数据的目的是希望每个线程可以独立的访问数据副本。而不需要担心与其他线程的同步访问问题。
比如全局变量 errno他是基本进程的。因为进程创建的时候,子进程会复制父进程的数据空间,那么子进程就拥有自己的全局变量errno。即使存在多个进程间的频繁的切换,也不会造成error的访问错误,因为他们都有自己的errno全局变量。
但是在多线程环境中就不行了。因为所有线程都会共享一个全局变量error。那么当线程频繁的切换时,很可能A线程判断的errno是B线程中的错误返回值。
所以要想让errno这种基于进程的接口能适应多线程环境中,那么就需要一种措施,时各个线程对errno的访问不会影响别的线程,
私有数据提供了解决这个问题的办法,errno被重新定义为线程的私有数据。这样所有的线程仍旧可以访问errno这个变量名,但其实他们真正访问的内容却是不一样的,访问的都是属于自己的私有数据。
下面是关于私有数据的一些接口。
在分配私有数据之前,需要创建于该数据关联的键,这个键用来获得对线程私有数据的访问,
int pthread_key_create(pthread_key_t *keyp,void (*destructor)(void *));
创建的键存放在keyp指向的内存单元中,这个键可以被进程中的所有线程使用,但是每个线程吧这个键与不同的线程私有数据地址进行关联。
destructor为与该键的关联的析构函数,他的参数就是私有数据的地址。如果destructor参数为NULL表明没有析构函数和键关联。
线程正常退出时会调用析构函数,非正常退出不会调用。(后面还会进一步说明)
int pthread_ket_delete(pthread_key_t *keyp);
用来取消键与线程私有数据之间的关联关系。
既然有了键与私有数据的关联性,那么所有的线程都只需要通过这个键来访问属于自己的私有数据,那么我们就需要保障这个键的唯一性。
如果像下面这个创建一个供所有线程使用的键
int first=1; //全局变量
。。。。
if(first==1){
first=2;
pthread_key_create(&key,destructor);
}
。。。。
这里在判断 first是不是等于一喝first修改为2之间有一个时间窗口,那么另一个线程可能会在这个窗口中也执行了一次 pthread_key_create造成了产生另外一个键,那么不同的线程之间看到的可能就不是一个键了。
所以为了确保键只被创建一次,提供了下面这个接口
pthread_once_t initflag=PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *initflag,void (*initfn)(void));
initflag参数必须是一个非本地的变量(需要是全局或静态的),而且必须初始化为PTHREAD_ONCE_INIT,如果每个线程都使用pthread_once,系统就能保证初始化历程initfn只被调用一次。
所以上面的存在竞争的创建键的方法应该改为下面的
pthread_once_t initflag=PTHREAD_ONCE_INIT;
void init_key(void){
pthread_key_create(&key,destructor);
}
void *thread_func(void *arg){
...
pthread_once(initflag,init_key);
}
这样就保证了键只会被创建一次。
最后我们需要把键与具体的私有数据关联起来。
void *pthread_getspecific(pthread_key_t key);
返回值:线程私有数据,若没有值与键相关联则返回NULL
int pthread_setspecific(pthread_key_t key,const void *value);
最后我们给出一个具体的例子。
意图是想让一个 key与两个线程 th1和th2 中各自的私有数据相关联。然后在 th1中利用pthread_getspecific得到线程 th1 与key关联的私有变量地址然后赋值给varoable(一个指针), 第一次修改 *variable=1 并在th1中打印出值。
然后切换到 th2 在 th2 中利用再次利用pthread_getspecific得到与th2 中与key相关联的私有数据地址,赋值给variable然后修改 *variable=2,并在th2 中打印。
最后我们再回到 th1中,因为variable是一个指针。那么在一般的程序中,如过使用形如 *variable=xx 就会将variable指向的
地址处的值改变。那么我们回到th1 再次调用pthread_getspecific获得 th1 与key关联的私有数据地址。然后输出。因为是私有数据
我们期望并不会像一般程序中一样会导致数据改变,他应该还是1
pthread_key_t key;
int *variable;
pthread_once_t initflag=PTHREAD_ONCE_INIT;
void init_key(void){
int err;
err=pthread_key_create(&key,free);
if(err!=0){
printf("create key error:%s\n",strerror(err));
exit(1);
}
}
void thread1_private(int **thread1_private_data){
int err;
err=pthread_once(&initflag,init_key);
if(err!=0){
printf("pthread_once error:%s\n",strerror(err));
exit(1);
}
*thread1_private_data=malloc(sizeof(int));
err=pthread_setspecific(key,*thread1_private_data);
if(err!=0){
printf("pthread_setspecific error:%s\n",strerror(err));
exit(1);
}
}
void thread2_private(int **thread2_private_data){
int err;
err=pthread_once(&initflag,init_key);
if(err!=0){
printf("pthread_once error:%s\n",strerror(err));
exit(1);
}
*thread2_private_data=malloc(sizeof(int));
err=pthread_setspecific(key,*thread2_private_data);
if(err!=0){
printf("pthread_setspecific error:%s\n",strerror(err));
exit(1);
}
}
void *th1(void *arg){
int *pthread1_private_data;
thread1_private(&pthread1_private_data);
variable=pthread_getspecific(key);
*variable=1;
printf("in thread_1 :variable=%d\n",*variable);
sleep(2);
variable=pthread_getspecific(key);
printf("in thread_1 :variable=%d\n",*variable);
pthread_exit((void *)0);
}
void *th2(void *arg){
int *pthread2_private_data;
thread2_private(&pthread2_private_data);
sleep(1);
variable=(int *)pthread_getspecific(key);
*variable=2;
printf("in thread_2:variable=%d\n",*variable);
pthread_exit((void *)0);
}
int main(){
pthread_t tid_1,tid_2;
int err;
pthread_create(&tid_1,NULL,th1,(void *)0);
pthread_create(&tid_2,NULL,th2,(void *)0);
err=pthread_join(tid_2,NULL);
if(err!=0){
printf("wait thread_2 error:%s\n",strerror(err));
exit(1);
}
err=pthread_join(tid_1,NULL);
if(err!=0){
printf("wait thread_1 error:%s\n",strerror(err));
exit(1);
}
exit(0);
}
最后我们查看输出看看 th1 中variable指向的内存的值是否改变了
in thread_1 :variable=1
in thread_2:variable=2
in thread_1 :variable=1
从输出 很容易看出来 th1中variable指向的值并为收th2的影响,也就是说每个线程与共享key关联的数据都是他们是私有的!
最后我们再来说一下与键关联的那个析构函数。前面说过该析构函数唯一的参数是私有数据的地址。如果传入的destroctor参数为NULL,表明没有析构函数和键相关联。当线程调用pthread_exit或者线程执行返回,正常退出,析构函数会被调用。但如果调用了exit,_exit,_Exit,abort或其他非正常方式退出时就不会调用析构函数
注意 ,当线程正常退出时,如果私有数据地址为NULL,那么析构函数也不会调用。
线程通常使用malloc为线程私有数据分配内存。析构函数通常用来释放分配的内存。
下面的测试 显示了析构函数的调用时间
4 pthread_key_t key;
5 void destructor(void *arg){
6 printf("in destructor\n");
7 free(arg);
8 }
9
10 void fun(void){
11
12 int *variable=NULL;
13 variable=malloc(sizeof(int));
14 pthread_key_create(&key,destructor);
15 pthread_setspecific(key,variable);
16 printf("fun done\n");
17 }
18
19 void *thread(void *arg){
20 fun();
21 printf("thread done\n");
22 pthread_exit((void *)0);
23 }
24
25 int main(void){
26 pthread_t tid;
27 pthread_create(&tid,NULL,thread,(void *)0);
28 pthread_join(tid,NULL);
29 printf("main done\n");
30 exit(0);
31 }
程序输出如下:
fun done
thread done
in destructor
main done
从输出顺序看出,destructor的确是在线程退出的时候才调用的。
如果把 第七 和 第十三 行注释掉,也就是私有数据地址为NULL,我们再来看看析构函数还会不会调用。
输出如下:
fun done
thread done
main done
正如我们上面所说的。当线程退出时,如果私有数据的地址为NULL,那么析构函数是不会被调用的。
阅读(2850) | 评论(1) | 转发(0) |