下面说一下线程中特有的线程存储, Thread Specific Data 。线程存储有什么用了?他是什么意思了?大家都知道,在多线程程序中,所有线程共享程序中的变量。现在有一全局变量,所有线程都可以使用它,改变它的值。而如果每个线程希望能单独拥有它,那么就需要使用线程存储了。表面上看起来这是一个全局变量,所有线程都可以使用它,而它的值在每一个线程中又是单独存储的。这就是线程存储的意义。 下面说一下线程存储的具体用法。 l 创建一个类型为 pthread_key_t 类型的变量。 l 调用 pthread_key_create() 来创建该变量。该函数有两个参数,第一个参数就是上面声明的 pthread_key_t 变量,第二个参数是一个清理函数,用来在线程释放该线程存储的时候被调用。该函数指针可以设成 NULL ,这样系统将调用默认的清理函数。 l 当线程中需要存储特殊值的时候,可以调用 pthread_setspcific() 。该函数有两个参数,第一个为前面声明的 pthread_key_t 变量,第二个为 void* 变量,这样你可以存储任何类型的值。 l 如果需要取出所存储的值,调用 pthread_getspecific() 。该函数的参数为前面提到的 pthread_key_t 变量,该函数返回 void * 类型的值。 下面是前面提到的函数的原型: int pthread_setspecific(pthread_key_t key, const void *value); void *pthread_getspecific(pthread_key_t key); int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
与进程不同,一个程序中的所有线程运行在同一个地址空间中。着表示如果一个线程修改了内存中的一个位置(例如,一个全局变量),则其它所有线程都会发现这个变化。因此多个线程可以同时操作同一块数据而不依赖进程间通信技术(在第五章中介绍了这些技术)。
尽管数据是共享的,每个线程都有单独的调用堆栈。因此每个线程都可以单独执行自己的代码,不加变化地调用子程序、从子程序返回。与在单线程程序中一样,每个线程每次调用子程序都会创造一组自己的局部变量;这些变量保存在线程自己的栈上。
不过有时仍然需要将一个变量复制给每个线程一个副本。GNU/Linux 系统通过为每个线程提供一个线程专有数据区(thread-specific data area)。当数据被存放在这个区域时会自动为每个线程创建一个副本;当一个线程修改自己的副本的时候并不会影响其它线程的副本。因为所有的线程共享一个 地址空间,线程专有数据不能通过普通的内存地址引用进行访问。GNU/Linux 系统提供了一系列函数用于读取和设置线程专有数据的值。
您想创建多少线程专有数据对象都可以;它们的类型都是 void*。每个数据对象都通过一个键值进行映射。要创建一个新键值从而为每个线程新创建一个数据对象,调用 pthread_key_create 函数。第一个参数是一个指向 pthread_key_t 类型变量的指针。新创建的键值将被保存在这个变量中(译者注:这句在原文中缺失,根据上下文意思以及 pthread_key_create(2) 手册页补充)。随后,这个键值可以被任意线程用于访问对应数据对象的属于自己的副本。传递给 pthread_key_create 的第二个参数是一个清理函数,如果您在这里传递一个函数指针,则 GNU/Linux 系统将在线程退出的时候以这个键值对应的数据对象为参数自动调用这个清理函数。清理函数非常有用,因为即使当线程在任何一个非特定运行时刻被取消,这个线 程函数也会被保证调用。如果对应的数据对象是一个空指针,清理函数将不会被调用。如果您不需要调用清理函数,您可以在这里传递空指针。
创建了键值之后,可以通过调用 pthread_setspecific 设定相应的线程专有数据值。第一个参数是键值,而第二个参数是一个指向要设置的数据的 void* 指针。以键值为参数调用 pthread_getspecific 可重新获取一个已经设置的线程专有数据。
假设这样一种情况,您的程序将一个任务分解以供多个线程执行。为了进行审计,每个线程都将分配一个单独的日志文件,用于记录对应线程的任务完成进度。线程专有数据区是为每个单独线程保存对应的日志文件指针的最方便的地点。
代码4.7展示了实现这个目标的一种方法。在这个例子中,main 函数创建了一个用于保存日志文件指针的线程专有数据区,随后将标识这个数据区的键值保存在 thread_log_key 中。因为 thread_log_key 是一个全局变量,所有线程共享对它的访问。每当一个线程开始执行的时候均会打开一个日志文件并由这个键值进行映射。之后,任何一个线程均可调用 write_to_thread_log 函数将一条信息写入线程对应的日志文件中。这个函数从线程专有数据中获取文件指针并将信息写入文件。
代码 4.7 (tsd.c)通过线程专有数据实现的线程日志
- #include <malloc.h>
-
#include <pthread.h>
-
#include <stdio.h>
- /* 用于为每个线程保存文件指针的 TSD 键值。*/
-
static pthread_key_t thread_log_key;
-
void write_to_thread_log(const char *message)
-
{
-
FILE* thread_log = (FILE *)pthread_getspecific(thread_log_key);
-
fprintf(thread_log, "%s\n", message);
-
}
-
-
void close_thread_log(void *thread_log)
-
{
-
fclose((FILE *)thread_log);
-
}
-
-
void *thread_function(void *args)
-
{
-
char thread_log_filename[20];
-
FILE *thread_log;
-
-
sprintf(thread_log_filename, "thread%d.log", (int)pthread_self());
- thread_log = fopen(thread_log_filename, "w");
- /* 将文件指针保存在thread_log_key标识的 TSD 中。*/
-
pthread_setspecific(thread_log_key, thread_log);
-
-
write_to_thread_log("Thread starting.");
-
-
return NULL;
-
}
-
-
int main()
-
{
-
int i;
-
pthread_t threads[5];
/* 创建一个键值,用于将线程日志文件指针保存在TSD中。 调用close_thread_log以关闭这些文件指针。*/
-
pthread_key_create(&thread_log_key, close_thread_log);
-
for(i = 0; i < 5; ++i)
-
pthread_create(&(threads[i]), NULL, thread_function, NULL);
-
-
for(i = 0; i < 5; ++i)
-
pthread_join(threads[i], NULL);
-
-
return 0;
-
- }
我们看到,thread_function 不需要关闭日志文件。这是因为当TSD键值被创建的时候我们将 close_thread_log 指定为这个 TSD 的清理函数。当线程退出的时候,GNU/Linux 将以 thread_log_key 所映射的值作为参数调用这个函数。这个函数会负责关闭文件指针。
清理句柄
线程专有数据的清理函数可以很有效地防止在线程退出或被取消的时候出现资源泄漏的问题。不过在有些情况下,我们希望创建一个清理函数却不希望为每个线程创建一个线程专有数据对象。出于这种需求,GNU/Linux 提供了清理句柄。
清理句柄就是一个当线程退出时被自动调用的函数。清理句柄函数接受一个 void* 类型的参数,且这个参数在注册清理句柄的时候被同时确定——这样就可以很方便地允许用同一个清理函数清理多份资源实例。
清理句柄是一个临时性的工具,只在当线程被取消或中途退出而不是正常结束运行的时候被调用。在一般情况下,程序应该显式释放分配的资源并清除已经设置的清理句柄。
通过提供两个参数(一个指向清理函数的函数指针和一个作为清理函数参数的void*类型的值)调用 pthread_cleanup_push
可以创建一个清理句柄。对 pthread_cleanup_push 的调用可以通过调用 pthread_cleanup_pop
进行平衡:pthread_cleanup_pop 会取消对一个句柄的注册。为简便操作起见,pthread_cleanup_pop 函数接受一个
int 类型的参数;如果这个参数为非零值,则在取消注册这个句柄的同时,清理句柄将被执行。
C++程序员习惯于通过将清理代码包装在对象析构函数中以获得“免费”的资源清理(译者注:C++重要设计原则RAII,Resource Acquisition is Initialization即是如此)。当由于当前块的结束或者由于C++异常的抛出导致对象的生命期结束的时候,C++确保自动对象的析构函数(如果 存在)会被自动调用。这对确保无论代码块如何结束均能调用清理代码块有很大的帮助。
但是,如果一个线程运行中调用了 pthread_exit,C++并不能保证线程的栈上所有自动对象的析构函数将被调用。不过可以通过一个很聪明的方法来获得这个保证:通过抛出一个特别设计的异常,然后在顶层的栈框架内再调用 pthread_exit 退出线程。
代码4.9中的程序展示了这种技巧。通过利用这个技巧,函数通过抛出一个 异常而不是直接调用 pthread_exit 来尝试退出线程。因为这个程序在顶层栈框架内被捕捉,当程序捕捉到异常的时候,所有在栈上分配的自动对象均已被销毁。
代码4.9 (cxx-exit.cpp)利用C++异常,安全退出线程
- #include <pthread.h>
-
-
class ThreadExitException
-
{
-
public:
-
/* 创建一个通过异常进行通知的线程退出方式。RETURN_VALUE 为线程返回值。*/
-
ThreadExitException (void* return_value)
-
: thread_return_value_ (return_value)
-
{
-
}
-
-
/* 实际退出线程。返回值由构造函数中指定。*/
-
void* DoThreadExit ()
-
{
-
pthread_exit (thread_return_value_);
-
}
-
private:
-
/* 结束线程时将使用的返回值。*/
-
void* thread_return_value_;
-
};
-
-
void do_some_work ()
-
{
-
while (1) {
-
/* 此处做有用工作... */
-
if (should_exit_thread_immediately ())
-
throw ThreadExitException (/* thread’s return value = */ NULL);
-
}
-
}
-
-
void* thread_function (void*)
-
{
-
try {
-
do_some_work ();
-
}
-
catch (ThreadExitException ex) {
-
/* 一些函数指示我们应该退出线程。 */
-
ex.DoThreadExit ();
-
}
-
return NULL;
- }