私有数据使用
1.为什么需要线程私有数据:
原因一:有时候需要维护基于每个线程的数据,用线程ID作为索引。因为线程ID不能保证是小而连续的整数,所以不能简单的分配一个线程数据数组,用线程ID作为数组的索引。即使线程ID确实是小而连续的整数,可能还希望有一些额外的保护,以防止某个线程的数据和其它线程的数据相混淆。
原因二:可以让基于进程的接口适应多线程环境,比如errno,线程出现以前errno被定义成进程环境中全局可访问的整数,线程出现以后,为了让线程也能使用那些原本基于进程的系统调用和库例程,errno被重新定义成线程私有数据。
(参考APUE2)
2.进程中的所有线程都可以访问进程的整个地址空间,除非使用寄存器(一个线程真正拥有的唯一私有存储是处理器寄存器),线程没有办法阻止其它线程访问它的数据,线程私有数据也不例外,但是管理线程私有数据的函数可以提高线程间的数据独立性。
3.int pthread_key_create(pthread_key_t *keyp, void (*destructor)(void *));
在分配(malloc)线程私有数据之前,需要创建和线程私有数据相关联的键(key),这个键的功能是获得对线程私有数据的访问权。
如果创建一个线程私有数据键,必须保证Pthread_key_create对于每个Pthread_key_t变量仅仅被调用一次,因为如果一个键被创建两次,其实是在创建两个不同的键,第二个键将覆盖第一个键,第一个键以及任何线程可能为其关联的线程私有数据值将丢失。
创建新键时,每个线程的数据地址设为NULL。
4.关于析构函数void (*destructor)(void *):
当线程退出时,如果线程私有数据地址被置为非NULL值,那么析构函数就会被调用。
当线程调用pthread_exit或者线程执行返回,正常退出时,析构函数就会被调用,但是如果线程调用了exit系列函数或者abort或者其它非正常退出时,就不会调用析构函数。
5.线程退出时,线程私有数据的析构函数将按照OS实现定义的顺序被调用。析构函数可能调用另外一个函数,而该函数可能创建新的线程私有数据而且把这个线程私有数据和当前的键关联起来。当所有的析构函数都调用完成以后,系统会检查是否有非NULL的线程私有数据值与键关联,如果有的话,再次调用析构函数,这个过程一直重复到线程所有的键都为NULL值线程私有数据,或者已经做了PTHREAD_DESTRUCTOR_ITERATIONS中定义的最大次数的尝试.
6.int pthread_delete(pthread_key_t *keyp),注意调用pthread_delete不会激活与键关联的析构函数,容易造成内存泄露。
当删除线程私有数据键的时候,不会影响任何线程对该键设置的线程私有数据值,甚至不影响调用线程当前键值,所以容易造成内存泄露,如果你不记得释放所有线程内与该键相关联的私有数据空间的话。
使用已经删除的私有数据键将导致未定义的行为。
编程建议:最后不删除线程私有数据键!!!尤其当一些线程仍然持有该键的值时,就更不该释放该键!!!
7.需要确保分配的键不会由于初始化阶段的竞争而发生变动。(使用pthread_once避免)
容易发生竞争的代码段:
void destructor(void *);
-
#include "../errors.h"
-
-
typedef struct tsd_tag{
-
pthread_t thread_id;
-
char *string;
-
}tsd_t;
-
-
pthread_key_t key;
-
pthread_once_t once = PTHREAD_ONCE_INIT;
-
-
void once_routine(void)
-
{
-
int status;
-
-
printf("Initializing key\n");
-
status = pthread_key_create(&key, NULL);
-
if(status != 0){
-
err_abort(status, "pthread_key_create");
-
}
-
}
-
-
void *thread_routine(void *arg)
-
{
-
int status;
-
tsd_t *value = NULL;
-
-
status = pthread_once(&once, once_routine);
-
if(status != 0){
-
err_abort(status, "pthread_once");
-
}
-
-
value = (tsd_t *)malloc(sizeof(tsd_t));
-
if(value == NULL){
-
errno_abort("malloc");
-
}
-
-
status = pthread_setspecific(key, (void *)value);
-
if(status != 0){
-
err_abort(status, "pthread_setspecific");
-
}
-
-
printf("%s set tsd value at %p\n", (char *)arg, value);
-
value->thread_id = pthread_self();
-
value->string = (char *)arg;
-
-
printf("%s starting......\n", (char *)arg);
-
sleep(2);
-
value = (tsd_t *)pthread_getspecific(key);
-
if(value == NULL){
-
printf("no thread-specific data value was associated \
-
with key\n");
-
pthread_exit(NULL);
-
}
-
printf("%s done......\n", value->string);
-
}
-
-
int main(int argc, char **argv)
-
{
-
pthread_t thread1, thread2;
-
int status;
-
-
status = pthread_create(&thread1, NULL, thread_routine, "thread 1");
-
if(status != 0){
-
err_abort(status, "create thread1");
-
}
-
-
status = pthread_create(&thread2, NULL, thread_routine, "thread 2");
-
if(status != 0){
-
err_abort(status, "create thread2");
-
}
-
-
pthread_exit(NULL);
-
}
运行结果
-
[xxxx@localhost chap5]$ ./a.out
-
Initializing key
-
thread 2 set tsd value at 0x8eab8e8
-
thread 2 starting......
-
thread 1 set tsd value at 0x8eab8d8
-
thread 1 starting......
-
thread 2 done......
-
thread 1 done......
私有数据原理探讨
线程私有数据实现的主要思想是:在分配线程私有数据之前,创建与该数据相关联的健,这个键可以被进程中的所有线程使用,但每个线程把这个键与不同的线程私有数据地址进行关联,需要说明的是每个系统支持有限数量的线程特定数据元素(下面的例子以128个为限制)。那么这个键的实现原理是什么呢?
其实系统为每个进程维护了一个称之为Key结构的结构数组,如下图所示:
(图1)
在上图中Key
结构的“标志”指示这个数据元素是否正在使用。在刚开始时所有的标志初始化为“不在使用”。当一个线程调用pthread_key_create创建一个新的线程特定数据元素时,系统会搜索Key结构数组,找出第一个“不在使用”的元素。并把该元素的索引(0~127)称为“键”。
返回给调用线程的正是这个索引。
除了进程范围内的Key结构数组之外,系统还在进程内维护了关于多个线程的多条信息。这些特定于线程的信息我们称之为Pthread结构。其中部分内容是我们称之为pkey数组的一个128个元素的指针数组。系统维护的关于每个线程的信息结构图如下:
(图2)
在上图中,pkey数组所有元素都被初始化为空指针。这些128个指针是和进程内128个可能的键逐一关联的值。
那么当我们调用pthread_key_create函数时,系统会为我们做什么呢?
系统首先会返回给我们一个Key结构数组中第一个“未被使用”的键(即索引值),每个线程可以随后通过该键找到对应的位置,并且为这个位置存储一个值(指针)。
一般来说,这个指针通常是每个线程通过调用malloc来获得的。
知道了大概的私有数据实现的原理,那么在编程中如何使用线程的特定数据呢?
假设一个进程被启动,并且多个线程被创建。
其中一个线程调用pthread_key_create。系统在Key结构数组(图1)中找到第1个未使用的元素。并把它的索引(0~127)返回给调用者。我们假设找到的索引为1
(我们会使用pthread_once 函数确保pthread_key_create只被调用一次,这个在以后会讲到)。
之后线程调用pthread_getspecific获取本线程的pkey[1] 的值(图(2)中键1所值的指针), 返回值是一个空值,线程那么调用malloc分配内存区并初始化此内存区。
之后线程调用pthread_setspecific把对应的所创建键的线程特定数据指针(pkey[1])
设置为指向它刚刚分配的内存区。下图指出了此时的情形。
(图三)
明白了怎样获取线程的特定数据值,那么如果线程终止时系统会执行什么操作呢?
我们知道,一个线程调用pthread_key_create创建某个特定的数据元素时,所指定的参数之一便是指向牧歌析构函数的指针。当一个线程终止时,系统将扫描该线程的pkey数组,为每个非空的pkey指针调用相应的析构函数。
相应的析构函数是存放在图1中的Key数组中的函数指针。这是一个线程终止时其线程特定数据的释放手段。
明白了线程私有数据的实现原理,我们就来看一下相应函数的用法:
#include
int phread_once(pthread_once_t *onceptr,
vid(*init)(void));
in pthread_key_create(pthread_key_t *keyptr,
void(* destructor)(void *value));
注意:pthread_once 使用onceptr
参数指向的变量中的值确保init参数所指的函数在进程范围内之被调用一次,onceptr必须是一个非本地变量(即全局变量或者静态变量),而且必须初始化为PTHREAD_ONCE_INIT。
阅读(1012) | 评论(0) | 转发(0) |