分类: 系统运维
2012-03-31 23:14:02
线程特定数据,也被称为线程私有数据,是一种存储和查找一个特定线程相关数据的机制。我们称这个数据为线程特定或线程私有的原因,是我们想每个线程访问它自己独立的数据拷贝,而不用担心和其它线程的访问的同步。
许多人在设计促进共享进程数据和属性的线程模型时碰到很多麻烦。所以为什么任何人在这个模型里都想要促进避免共享的接口呢?有两个原因。
首先,有时我们需要为每个线程维护数据。因为没有保证线程ID是小的连续的整型,所以我们不能简单地为线程数据分配一个数组,并使用线程ID作为索引。即使我们可以依赖于小的连续的线程ID,我们仍想要一些额外的保护以便一个线程不会和破坏另一个的数据。
第 二个线程私有数据的原因是为了提供把基于进程的接口适配到一个多线程环境的一个机制。一个明显的例子是errno。回想1.7节errno的讨论。更老的 接口(在线程的出现之前)定义errno作为在进程上下文里全局访问的一个整型。系统调用和库例程设置errno作为失败的一个副作用。为了使线程可以使 用这些相同的系统调用和库例程,errno被重新定义为线程私有数据。因而,一个调用设置errno的函数的线程不会影响进程里其它线程的errno值。
回想进程里的所有线程访问进程的整个地址空间。除了使用寄存器,没有办法让一个线程阻止另一个访问它的数据。即使对于线程特定数据也是如此。尽管底下的实现不阻止访问,然而为管理线程特定数据而提供的函数促进了线程间的数据分离。
创建的关键字被存储在keyp所指的内存位置。这个相同的关键字可以被进程里所有线程使用,但是每个线程将使用这个关键字关联于一个不同的线程特定数据地址。当创建字被创建时,每个线程的数据地址被设为空值。
除 了创建一个关键字,pthread_key_create还把一个可选的析构函数关联于这个关键字。当线程退出时,如果数据地址被设置为非空值,析构函数 被创建,这个数据地址被作为唯一的参数。如果destructor为空,那么没有析构函数被关联到这个关键字。当线程通过调用pthread_exit或 return正常退出时,destructor被调用。但是如果线程调用exit、_exit或_Exit或abort,或其它异常退出,那么析构体不被 调用。
线程通常使用malloc来为它们的线程特定数据分配内存。析构函数通常释放被分配的内存。如果线程退出时不释放内存,内存将被丢失:被进程泄露。
一 个线程可以为线程特定数据分配多个关键字。每个关键字可以有一个与它关联的析构体。每个关键字可以有一个不同的析构函数,它们也可以全部使用相同的函数。 每个操作系统实现可以为一个进程可以分配的关键字数设置一个限量(回想12.2节的PTHREAD_KEYS_MAX)。
当一个线程退出 时,它的线程特定数据的析构体以一个实现定义的顺序被调用。析构函数可能调用另一个可能创建新的线程特定数据并把它关联到这个关键字的函数。在所有析构体 被调用后,系统将检查任何非空线程特定数据是否与关键字相关联,如果有,再次调用析构体。这个过程会重复,直到线程的所有关键字都有空的线程特定数据值, 或者最多尝试了PTHREAD_DESTRUCTOR_ITERATIONS(12.2节)次。
我们可以为所有线程打破关键字和线程特定数据值的关联,通过调用pthread_key_delete。
注意调用pthread_key_delete将不会调用key相关的析构体。要释放任何与key的线程特定数据相关的内存,我们必须在应用里采取额外的步骤。
我们需要确保我们分配的一个关键字不会因为初始化期间的一个竞争而改变。像下方的代码可以导致两个线程都调用pthread_key_create:
void destructor(void *);
pthread_key_t key;
int init_done = 0;
int
thread_func(void *arg)
{
if (!init_done) {
init_done = 1;
err = pthread_key_create(&key, destructor);
}
...
}
取决于系统如果调度线程,一些线程可能看到一个关键字值,而另一个线程可能看到一个不同的值。解决这个竞争的方法是使用pthread_once。
initflag必须是一个非局部变量(也就是全局或静态的)并初始化为PTHREAD_ONCE_INIT。
如果每个线程都调用pthread_once,那么系统保证初始化例程initfn将只被调用一次,在第一次调用pthread_once时。没有竞争的创建一个关键字的恰当的方法如下:
void destructor(void *)
pthread_key_t key;
pthread_once_t init_done = PTHREAD_ONCE_INIT;
void
thread_init(void)
{
err = pthread_key_create(&key, destructor);
}
int
threadfunc(void *arg)
{
pthread_once(&init_done, thread_init);
...
}
一旦一个关键字被创建,我们就可以把线程特定数据关联到这个关键字,通过调用pthread_setspecific。我们可以用pthread_getspecific来得到线程特定数据的地址。
如果没有线程特定数据被关联到一个关键字,那么pthread_getspecific将返回一个空指针。我们可以使用它来确定是否需要调用pthread_setspecific。
在 上一节,我们展示了getenv的一个猜想的实现。之后创建一个新的接口,但是以线程安全的方式提供了相同的功能。但是如果我们不想修改应用程序来使用新 的接口会发生什么呢?在这种情况下,我们可以使用线程特定数据来为每个线程维护一个用来保存返回的字符串的数据缓冲的拷贝。由下面的代码展示。
注意尽管这个版本的getenv是线程安全的,但它不是异步信号安全的。即使我们让互斥体递归,我们也不能让它关于信号处理机可再入,因为它调用了malloc,它自身就不是异步信号安全的。