线程私有数据(也称线程特定数据)是存储和查询与某个线程相关的数据的一种机制。把这种数据称为线程私有数据或线程特定数据的原因是:希望每个线程可以独立地访问数据副本,而不需要担心与其他线程的同步访问问题。
线程模型促进了进程中数据和属性的共享,许多人在设计线程模型时会遇到各种麻烦。但在这样的模型中,为什么还需要提出一些合适的用于阻止共享的接口呢?其中有两个原因:
第一,有时候需要维护基于每个线程的数据。
采用线程私有数据的第二个原因是:它提供了让基于进程的接口适应多线程环境的机制。一个很明显的实例就是errno。回忆http://www.cnblogs.com/nufangrensheng/p/3495426.html中对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指向的内存单元,这个键可以被进程中的所有线程使用,但每个线程把这个键与不同的线程私有数据地址进行关联(如何关联???)。创建新键时,每个线程的数据地址设为null值。
除了创建键以外,pthread_key_create可以选择为该键关联析构函数,当线程退出时,如果数据地址已经被置为非null数值,那么析构函数就会被调用,它唯一的参数就是该数据地址。如果传入的destructor参数为null,就表明没有析构函数与键关联。当线程调用pthread_exit或者线程执行返回,正常退出时,析构函数就会被调用,但如果线程调用了exit、_exit、_Exit、abort或出现其他非正常的退出时(关于正常退出与不正常退出:http://www.cnblogs.com/nufangrensheng/p/3509618.html),就不会调用析构函数。
线程通常使用malloc为线程私有数据分配内存空间,析构函数通常释放已分配的内存。如果线程没有释放内存就退出了,那么这块内存将会丢失,即线程所属进程出现了内存泄漏。
线程可以为线程私有数据分配多个键,每个键都可以有一个析构函数与它关联。各个键的析构函数可以互不相同,当然它们也可以使用相同的析构函数。每个操作系统在实现的时候可以对进程可分配的键的数量进行限制(回忆http://www.cnblogs.com/nufangrensheng/p/3522577.html中表12-1中的PTHREAD_KEYS_MAX)。
线程退出时,线程私有数据的析构函数将按照操作系统实现中定义的顺序被调用。析构函数可能会调用另一个函数,该函数可能会创建新的线程私有数据而且把这个数据与当前的键关联起来。当所有的析构函数都调用完成以后,系统会检查是否还有非null的线程私有数据值与键关联,如果有的话,再次调用析构函数。这个过程会一直重复直到线程所有的键都为null值线程私有数据,或者已经做了PTHREAD_DESTRUCTOR_ITERATIONS(http://www.cnblogs.com/nufangrensheng/p/3522577.html中表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; thread_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-5 线程安全的getenv的兼容版本(使用线程私有数据来维护每个线程的数据缓冲区的副本,用于存放各自的返回字符串)
#include <limits.h> #include <string.h> #include <pthread.h> #include <stdlib.h> 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 pthread_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(ARG_MAX); 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] == ‘=‘)) { strcpy(envbuf, &environ[i][len+1]); pthread_mutex_unlock(&env_mutex); return(envbuf); } } pthread_mutex_unlock(&env_mutex); return(NULL); }
使用pthread_once来确保只为将要使用的线程私有数据创建了一个键。如果pthread_getspecific返回的是空指针,需要分配内存然后把键与该内存单元关联,否则如果返回的不是空指针,就是用pthread_getspecific返回的内存单元。对析构函数,使用free来释放之前由malloc分配的内存。只有当线程私有数据值为非null时,析构函数才会被调用。
注意,虽然这个版本的getenv是线程安全的,但它并不是异步-信号安全的。对信号处理程序而言,即使使用递归的互斥量,这个版本的getenv也不可能是可重入的,因为它调用了malloc,而malloc函数本身并不是异步-信号安全的。
本篇博文内容摘自《UNIX环境高级编程》(第二版),仅作个人学习记录所用。关于本书可参考:http://www.apuebook.com/。