线程特定数据(thread-specific data),也称为线程私有数据(thread-private data),是存储和查询某个特定线程相关数据的一种机制。我们把这种数据称为线程特定数据或线程私有数据的原因是,我们希望每个线程可以访问它自己单独的数据副本,而不需要担心与其他线程的同步访问问题。
线程模型促进了进程中数据和属性的共享,许多人在设计线程模型时会遇到各种麻烦。那么为什么有人想在这样的模型中促进阻止共享的接口呢?这其中有两个原因。
- 第一,有时候需要维护基于每线程(perthread)的数据。因为线程ID并不能保证是小而连续的整数,所以就不能简单地分配一个每线程数据数组,用线程ID作为数组的索引。即使线程ID确实是小而连续的整数,我们可能还希望有一些额外的保护,防止某个线程的数据与其他线程的数据相混淆。
- 采用线程私有数据的第二个原因是,它提供了让基于进程的接口适应多线程环境的机制。一个很明显的实例就是
errno
。回忆1.7节中对errno
的讨论。以前的接口(线程出现以前)把errno
定义为进程上下文中全局可访问的整数。系统调用和库例程在调用或执行失败时设置errno
,把它作为操作失败时的附属结果。为了让线程也能够使用那些原本基于进程的系统调用和库例程,errno
被重新定义为线程私有数据。这样,一个线程做了重置errno
的操作也不会影响进程中其他线程的errno
值。
我们知道一个进程中的所有线程都可以访问这个进程的整个地址空间。除了使用寄存器以外,一个线程没有办法阻止另一个线程访问它的数据。线程特定数据也不例外。虽然底层的实现部分并不能阻止这种访问能力,但管理线程特定数据的函数可以提高线程间的数据独立性,使得线程不太容易访问到其他线程的线程特定数据。
在分配线程特定数据之前,需要创建与该数据关联的键。这个键将用于获取对线程特定数据的访问。使用pthread_key_create
创建一个键。
#include <pthread.h>
int pthread_key_create(pthread_key_t *keyp, void(*destructor)(void *));
//返回值:若成功,返回0;否则,返回错误编号
创建的键存储在keyp
指向的内存单元中,这个键可以被进程中的所有线程使用,但每个线程把这个键与不同的线程特定数据地址进行关联。创建新键时,每个线程的数据地址设为空值。
除了创建键以外,pthread_key_create
可以为该键关联一个可选择的析构函数。当这个线程退出时,如果数据地址已经被置为非空值,那么析构函数就会被调用,它唯一的参数就是该数据地址。如果传入的析构函数为空,就表明没有析构函数与这个键关联。当线程调用pthread_exit
或者线程执行返回,正常退出时,析构函数就会被调用。同样,线程取消时,只有在最后的清理处理程序返回之后,析构函数才会被调用。如果线程调用了exit
、_exit
、_Exit
或abort
,或者出现其他非正常的退出时,就不会调用析构函数。
线程通常使用malloc
为线程特定数据分配内存。析构函数通常释放已分配的内存。如果线程在没有释放内存之前就退出了,那么这块内存就会丢失,即线程所属进程就出现了内存泄漏。
线程可以为线程特定数据分配多个键,每个键都可以有一个析构函数与它关联。每个键的析构函数可以互不相同,当然所有键也可以使用相同的析构函数。每个操作系统实现可以对进程可分配的键的数量进行限制(回忆一下图12-1中的PTHREAD_KEYS_MAX
)。
线程退出时,线程特定数据的析构函数将按照操作系统实现中定义的顺序被调用。析构函数可能会调用另一个函数,该函数可能会创建新的线程特定数据,并且把这个数据与当前的键关联起来。当所有的析构函数都调用完成以后,系统会检查是否还有非空的线程特定数据值与键关联,如果有的话,再次调用析构函数。这个过程将会一直重复直到线程所有的键都为空线程特定数据值,或者已经做了PTHREAD_DESTRUCTOR_ITERATIONS
(见图12-1)中定义的最大次数的尝试。
对所有的线程,我们都可以通过调用pthread_key_delete
来取消键与线程特定数据值之间的关联关系。
#include <pthread.h>
int pthread_key_delete(pthread_key_t key);
//返回值:若成功,返回0;否则,返回错误编号
注意,调用pthread_key_delete
并不会激活与键关联的析构函数。要释放任何与键关联的线程特定数据值的内存,需要在应用程序中采取额外的步骤。
需要确保分配的键并不会由于在初始化阶段的竞争而发生变动。下面的代码会导致两个线程都调用pthread_key_create
。
void destructor(void *);
pthread_key_t key;
int init_done = 0;
int threadfunc(void *arg){
if (!init_done) {
init_done = 1;
err = pthread_key_create(&key, destructor);
}
//...
}
有些线程可能看到一个键值,而其他的线程看到的可能是另一个不同的键值,这取决于系统是如何调度线程的,解决这种竞争的办法是使用pthread_once
。
#include <pthread.h>
pthread_once_t initflag = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *initflag, void(*initfn)(void));
//返回值:若成功,返回0;否则,返回错误编号
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
函数获得线程特定数据的地址。
#include <pthread.h>
void *pthread_getspecific(pthread_key_t key);
//返回值:线程特定数据值;若没有值与该键关联,返回NULL
int pthread_setspecific(pthread_key_t key, const void* value);
//返回值:若成功,返回0;否则,返回错误编号
如果没有线程特定数据值与键关联,pthread_getspecific
将返回一个空指针,我们可以用这个返回值来确定是否需要调用pthread_setspecific
。
实例
图12-11给出了getenv
的假设实现。接着又给出了一个新的接口,提供的功能相同,不过它是线程安全的(见图12-12)。但是如果不修改应用程序,直接使用新的接口会出现什么问题呢?这种情况下,可以使用线程特定数据来维护每个线程的数据缓冲区副本,用于存放各自的返回字符串,如图12-13所示。
#include <limits.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#define MAXSTRINGSZ 4096
static pthread_key_t key;
static pthread_once_t init_done = PTHREAD_ONCE_INIT;
pthread_mutex_t env_mutex = PTHREAD_MUTEX_INITIALIZER;
extern char **environ;
static void
thread_init(void)
{
pthread_key_create(&key, free);
}
char *
getenv(const char *name)
{
int i, len;
char *envbuf;
pthread_once(&init_done, thread_init);
pthread_mutex_lock(&env_mutex);
envbuf = (char *)pthread_getspecific(key);
if (envbuf == NULL) {
envbuf = malloc(MAXSTRINGSZ);
if (envbuf == NULL) {
pthread_mutex_unlock(&env_mutex);
return(NULL);
}
pthread_setspecific(key, envbuf);
}
len = strlen(name);
for (i = 0; environ[i] != NULL; i++) {
if ((strncmp(name, environ[i], len) == 0) &&
(environ[i][len] == '=')) {
strncpy(envbuf, &environ[i][len+1], MAXSTRINGSZ-1);
pthread_mutex_unlock(&env_mutex);
return(envbuf);
}
}
pthread_mutex_unlock(&env_mutex);
return(NULL);
}
图12-13 线程安全的getenv的兼容版本
我们使用 pthread_once
来确保只为我们将使用的线程特定数据创建一个键。如果pthread_getspecific
返回的是空指针,就需要先分配内存缓冲区,然后再把键与该内存缓冲区关联。否则,如果返回的不是空指针,就使用pthread_getspecific
返回的内存缓冲区。对析构函数,使用free
来释放之前由malloc
分配的内存。只有当线程特定数据值为非空时,析构函数才会被调用。
注意,虽然这个版本的getenv
是线程安全的,但它并不是异步信号安全的。对信号处理程序而言,即使使用递归的互斥量,这个版本的getenv
也不可能是可重入的,因为它调用了malloc
,而malloc
函数本身并不是异步信号安全的。