12.6 线程私有数据
线程私有数据是一种用于存储和获取与特定线程相关联数据的机制,称为线程特定的或者是线程私有的,是因为我们希望每个线程都可以独立访问其独有的数据,而不用担心与其他线程的同步访问问题。
许多人费力实现了促进进程数据以及属性贡献的线程模型,那么为什么还有人想要实现一个接口,在这样一个模型中防止共享呢?有如下两点原因:
首先,有些时候我们需要以线程为基础维护一些数据,因为没有任何机制可以保证线程ID总是比较小的,且是连续的整数,因此我们不能简单地将每一个线程的私有数据分配为一个数组,然后使用线程ID作为索引进行访问。即使我们可以保证线程ID是比较小的,且是连续的整数。我们仍然还需要一些额外的保护,保证一个线程不会搞乱别的线程的数据。
其次,线程私有数据可以提供一种机制实现基于线程的接口可以适应多线程环境。一个很明显的例子是errno,在1.7节中提到,老版本的程序接口(在线程出现之前)定义errno作为一个整形变量,可以在进程范围内进行全局访问,在系统调用以及线程函数失败的时候可以设置errno.为了使得多线程系统使用相同的系统调用以及库函数成为可能,errno被重新定义为线程私有数据,如此一来,一个线程的函数调用修改了errno并不会导致进程内其他线程的errno的数值受到任何影响。
前文有讲到,在一个进程内的所有线程都有对进程内整个地址空间的访问权限,除非是使用寄存器,否则没有办法防止一个线程访问其他线程的数据。即使是对于线程私有数据仍然是成立的。虽然底层实现并没有限制线程私有数据在线程间的访问,但是管理线程私有数据的函数可以使得其他线程访问线程私有数据的时候更加困难。
在分配线程私有数据之前,我们需要创建一个key,用于实现与数据的联系。这个key将用于增加对于线程私有数据的访问。我们可以使用函数pthread_key_create来创建这样一个key.
#include <pthread.h>
int pthread_key_create(pthread_key_t *keyp, void (*destructor)(void *));
Returns: 0 if OK, error number on failure.
函数创建的key存储在keyp指针指定的内存区域,同一个key可以被同一个进程内的所有线程使用,但是每个线程都将会与这个key关联一个不同的线程私有数据地址,当key创建的时候,每个线程的数据地址被初始化为null值;
上述函数不仅创建了一个key,还关联了一个可选的destructor函数。当线程退出的时候,如果数据地址已经被设置为non-nuil数值,destructor函数将会被调用,并使用其数据地址作为唯一的参数。如果destructor指针是null,那么就不会由destructor函数与key相关联。当线程正常退出的时候,无论是调用函数pthrad_exit还是返回,destructor都会被调用,同样地,如果线程被取消,destructor也会被调用,但是是在最后一个cleanup handler返回之后才被调用,但是如果线程调用函数exit,_exit,_Exit或者是abort,或者是其他的异常退出方式,destructor函数并不会被调用。
线程通常使用malloc函数分配线程私有数据的存储空间,destructor函数通常用于释放之前分配的内存。如果线程退出的时候没有释放之前分配的内存,那么内存就会逐渐泄漏。
一个线程可以为线程私有数据分配多个key.每一个key可以由一个destructor与之关联,可能对于每一个key都有一个不同的destructor函数,或者所有的key使用相同的destructor函数,每一个操作系统实现可能会对一个线程可以分配的key数量进行了限制(在12.2节中提到的PTHREAD_KEYS_MAX).
当线程退出的时候,destructor函数将会按照实现定义的顺序进行调用,destructor函数调用另外的函数创建一个新的线程私有数据并将其与key相关联是可能的。在所有的destructor被调用完成之后,系统会检查是否还有与key相关联的非空线程私有数据指针,如果由,就再次调用destructor函数。进程将会重复这一过程,直到所有的key的所有关联的线程私有数据是null的,或者尝试的调用次数达到最大值PTHREAD_DESTRUCTOR_ITERATIONS(图12.1)。
我们可以使用函数pthread_key_delete来删除所有线程私有数据与指定key的联系。
#include <pthread.h>
int pthread_key_delete(pthread_key_t key);
Returns: 0 if OK, error number on failure.
需要注意的是,调用函数pthread_key_delete并不会调用析构函数destructor,为了释放与key相关联的线程私有数据,我们需要在应用程序中增加一些额外的步骤。
此外,我们还需要注意我们分配的key不能由于竞态的初始化过程而发生改变。如下所示的程序中,两个线程都可能会调用函数pthread_key_create:
void destructor(void *);
pthread_key_t key;
int init_code = 0;
int threadfunc(void *arg)
{
if(! init_done)
{
init_done = 1;
err = pthread_key_create(&key, destructor);
}
}
与操作系统的调度方式有关,一部分线程可能读取到的是一个key值,但是另一些线程可能读取到的是另外一个不同的值,解决这一竞态条件的方式是调用函数pthtread_once.
#include <pthread.h>
pthread_once_t initflag = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *initflag, void (*initfn)(void));
Returns: 0 if OK, error number on failure.
注意::::::::::::initflag一定不能是本地变量,可以是全局的或者是静态的,并且需要初始化为数值PTHREAD_ONCE_INIT.
如果一个线程调用了函数pthread_once, 系统就会保证初始化函数Initfn仅仅被调用一次,并且仅仅出现在第一次指定函数pthread_once调用的时候,创建一个key的正确的方法是这样的:
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);
...
}
一旦key创建完成,我们就可以将线程私有数据与key进行关联了,这一步骤需要调用函数pthread_setspecific来完成,同时,我们也可以使用函数pthread_getspecific函数来获取线程私有数据的地址;
#include <pthread.h>
void *pthread_getspecific(pthread_key_t key);
Returns:thread-specific data value or NULL if no value has been associated with the key.
int pthread_setspecific(pthread_key_t key, const void *value);
Returns: 0 if OK, error number on failure.
如果没有线程私有数据与key相关联,函数pthread_getspecific就会的返回一个空指针,我们可以利用这一点来决定是否需要调用函数pthread_setspecific.
Example
在图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;
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函数保证对于线程私有数据相关联的key我们仅仅会创建一次。如果函数pthread_getspecific返回一个null指针,我们就需要分配内存缓冲区并将其与key相关联。否则,我们直接使用pthread_getspecific返回的缓冲区进行操作,对于析构函数,我们使用的是free,如果线程私有数据不为空的话,那么析构函数就会以私有数据为参数进行调用。
注意,即使上述的getenv函数版本是线程安全的,但是仍然不是异步信号安全的,即是我们修改互斥锁为递归互斥锁,我们也不能使其在信号处理函数中变得可重入,因为函数内部还调用了函数malloc,malloc函数本身就不是异步信号安全的。