线程私有数据(Thread-specific data,TSD):存储和查询与某个线程相关数据的一种机制。来自http://blog.csdn.net/ctthuangcheng/article/details/9357917
- 在进程内的所有线程都共享相同的地址空间,即意味着任何声明为静态或外部变量,或在进程堆声明的变量,都可以被进程内所有的线程读写。
- 一个线程真正拥有的唯一私有存储是处理器寄存器,栈在“主人”故意暴露给其他线程时也是共享的。
- 有时需要提供线程私有数据:可以跨多个函数访问(全局);仅在某个线程有效(私有)(即在线程里面是全局)。例如:errno。
进程中的所有线程都可以访问进程的整个地址空间,除非使用寄存器(一个线程真正拥有的唯一私有存储是处理器寄存器),线程没有办法阻止其它线程访问它的数据,线程私有数据也不例外,但是管理线程私有数据的函数可以提高线程间的数据独立性。
进程内的所有线程共享进程的数据空间,因此全局变量为所有线程所共有。但有时线程也需要保存自己的私有数据,这时可以创建线程私有数据(Thread-specific Date)TSD来解决。在线程内部,私有数据可以被各个函数访问,但对其他线程是屏蔽的。例如我们常见的变量errno,它返回标准的出错信息。它显然不能是一个局部变量,几乎每个函数都应该可以调用它;但它又不能是一个全局变量。(即在线程里面是全局变量)
创建线程私有数据就是为了线程内部各个函数可以很容易的传递数据信息,因此要使线程外的函数不能访问这些数据,而线程内的函数使用这些数据就像线程内的全局变量一样,这些数据在一个线程内部是全局的,一般用线程私有数据的地址作为线程内各个函数访问该数据的入口。
线程私有数据采用了一种被称为一键多值的技术,即一个键对应多个数值。访问数据时都是通过键值来访问,好像是对一个变量进行访问,其实是在访问不同的数据。使用线程私有数据时,首先要为每个线程私有数据创建一个相关联的键。在各个线程内部,都使用这个公用的键来指代线程数据,但是在不同的线程中,这个键代表的数据是不同的。操作线程私有数据的函数主要有4个:pthread_key_create(创建一个键),pthread_setspecific(为一个键设置线程私有数据),pthread_getspecific(从一个键读取线程私有数据),pthread_key_delete(删除一个键)。
创建一个键:
- int pthread_key_create(pthread_key_t *keyp, void (*destructor)(void *));
在分配(malloc)线程私有数据之前,需要创建和线程私有数据相关联的键(key),这个键的功能是获得对线程私有数据的访问权。
如果创建一个线程私有数据键,必须保证pthread_key_create对于每个Pthread_key_t变量仅仅被调用一次,因为如果一个键被创建两次,其实是在创建两个不同的键,第二个键将覆盖第一个键,第一个键以及任何线程可能为其关联的线程私有数据值将丢失。
创建新键时,每个线程的私有数据地址设为NULL。
注意:创建的键存放在keyp指向的内存单元,这个键可以被进程中的所有线程使用,但每个线程把这个键与不同的线程私有数据地址进行关联。
除了创建键以外,pthread_key_create可以选择为该键关联析构函数,当线程退出时,如果线程私有数据地址被置为非NULL值,那么析构函数就会被调用。
注意:析构函数参数为退出线程的私有数据的地址。如果私有数据的地址为NULL,就说明没有析构函数与键关联即不需要调用该析构函数。
当线程调用pthread_exit或者线程执行返回,正常退出时,析构函数就会被调用,但是如果线程调用了exit、_exit、Exit函数或者abort或者其它非正常退出时,就不会调用析构函数。
线程通常使用malloc为线程私有数据分配空间,析构函数通常释放已分配的线程私有数据的内存。
线程可以为线程私有数据分配多个键,每个键都可以有一个析构函数与它关联。各个键的析构函数可以互不相同,当然它们也可以使用相同的析构函数。
线程退出时,线程私有数据的析构函数将按照操作系统实现定义的顺序被调用。析构函数可能调用另外一个函数,而该函数可能创建新的线程私有数据而且把这个线程私有数据和当前的键关联起来。当所有的析构函数都调用完成以后,系统会检查是否有非NULL的线程私有数据值与键关联,如果有的话,再次调用析构函数,这个过程一直重复到线程所有的键都为NULL值线程私有数据,或者已经做了PTHREAD_DESTRUCTOR_ITERATIONS中定义的最大次数的尝试。
示例代码:
- #include <stdio.h>
- #include <pthread.h>
- #include <stdlib.h>
-
- typedef struct private_tag {
- pthread_t thread_id;
- char *string;
- } private_t;
-
- pthread_key_t identity_key;
- pthread_mutex_t identity_key_mutex = PTHREAD_MUTEX_INITIALIZER;
- long identity_key_counter = 0;
-
-
- void identity_key_destructor (void *value)
- {
- private_t *private = (private_t*)value;
- int status;
-
- printf ("thread \"%s\" exiting...\n", private->string);
- free (value);
- status = pthread_mutex_lock (&identity_key_mutex);
- if (status != 0)
- perror("pthread_mutex_lock");
- identity_key_counter--;
- if (identity_key_counter <= 0) {
- status = pthread_key_delete (identity_key);
- if (status != 0)
- perror("pthread_key_delete");
- printf ("key deleted...\n");
- }
- status = pthread_mutex_unlock (&identity_key_mutex);
- if (status != 0)
- perror("pthread_mutex_unlock");
- }
-
- void *identity_key_get (void)
- {
- void *value;
- int status;
-
- value = pthread_getspecific (identity_key);
- if (value == NULL) {
- value = malloc (sizeof (private_t));
- if (value == NULL)
- perror ("malloc");
- status = pthread_setspecific (identity_key, (void*)value);
- if (status != 0)
- perror("pthread_setspecific");
- }
- return value;
- }
-
- void *thread_routine (void *arg)
- {
- private_t *value;
-
- value = (private_t*)identity_key_get ();
- value->thread_id = pthread_self ();
- value->string = (char*)arg;
- printf ("thread \"%s\" starting...\n", value->string);
- sleep (2);
- return NULL;
- }
-
- void main (int argc, char *argv[])
- {
- pthread_t thread_1, thread_2;
- private_t *value;
- int status;
-
- status = pthread_key_create (&identity_key, identity_key_destructor);
- if (status != 0)
- perror("pthread_key_create");
- identity_key_counter = 3;
- value = (private_t*)identity_key_get ();
- value->thread_id = pthread_self ();
- value->string = "Main thread";
- status = pthread_create (&thread_1, NULL,thread_routine, "Thread 1");
- if (status != 0)
- perror("pthread_create");
- status = pthread_create (&thread_2, NULL,thread_routine, "Thread 2");
- if (status != 0)
- perror("pthread_create");
- pthread_exit (NULL);
- }
运行结果:
- huangcheng@ubuntu:~$ ./a.out
- thread "Main thread" exiting...
- thread "Thread 2" starting...
- thread "Thread 1" starting...
- thread "Thread 2" exiting...
- thread "Thread 1" exiting...
- key deleted...
- huangcheng@ubuntu:~$
示例代码2:
- #include <stdio.h>
- #include <pthread.h>
- #include <stdlib.h>
-
- typedef struct tsd_tag{
- pthread_t thread_id;
- char *string;
- }tsd_t;
-
- pthread_key_t key;
- pthread_once_t once = PTHREAD_ONCE_INIT;
-
- void once_routine(void)
- {
- int status;
-
- printf("Initializing key\n");
- status = pthread_key_create(&key, NULL);
- if(status != 0){
- perror("pthread_key_create");
- }
- }
-
- void *thread_routine(void *arg)
- {
- int status;
- tsd_t *value = NULL;
-
- status = pthread_once(&once, once_routine);
- if(status != 0){
- perror("pthread_once");
- }
-
- value = (tsd_t *)malloc(sizeof(tsd_t));
- if(value == NULL){
- perror("malloc");
- }
-
- status = pthread_setspecific(key, (void *)value);
- if(status != 0){
- perror("pthread_setspecific");
- }
-
- printf("%s set tsd value at %p\n", (char *)arg, value);
- value->thread_id = pthread_self();
- value->string = (char *)arg;
-
- printf("%s starting......\n", (char *)arg);
- sleep(2);
- value = (tsd_t *)pthread_getspecific(key);
- if(value == NULL){
- printf("no thread-specific data value was associated \
- with key\n");
- pthread_exit(NULL);
- }
- printf("%s done......\n", value->string);
- }
-
- int main(int argc, char **argv)
- {
- pthread_t thread1, thread2;
- int status;
-
- status = pthread_create(&thread1, NULL, thread_routine, "thread 1");
- if(status != 0){
- perror("pthread_create");
- }
-
- status = pthread_create(&thread2, NULL, thread_routine, "thread 2");
- if(status != 0){
- perror("pthread_create");
- }
-
- pthread_exit(NULL);
- }
运行结果:
- huangcheng@ubuntu:~$ ./a.out
- Initializing key
- thread 2 set tsd value at 0x8fb7520
- thread 2 starting......
- thread 1 set tsd value at 0x8fb7530
- thread 1 starting......
- thread 2 done......
- thread 1 done......
示例代码3:
- #include <stdio.h>
- #include <stdlib.h>
- #include <pthread.h>
-
- pthread_key_t key;
-
- struct test_struct {
- int i;
- float k;
- };
-
-
- void *child1 (void *arg)
- {
- struct test_struct struct_data;
-
- struct_data.i = 10;
- struct_data.k = 3.1415;
-
- pthread_setspecific (key, &struct_data);
- printf ("结构体struct_data的地址为 0x%p\n", &(struct_data));
- printf ("child1 中 pthread_getspecific(key)返回的指针为:0x%p\n", (struct test_struct *)pthread_getspecific(key));
-
- printf ("利用 pthread_getspecific(key)打印 child1 线程中与key关联的结构体中成员值:\nstruct_data.i:%d\nstruct_data.k: %f\n", ((struct test_struct *)pthread_getspecific (key))->i, ((struct test_struct *)pthread_getspecific(key))->k);
-
- printf ("------------------------------------------------------\n");
- }
-
- void *child2 (void *arg)
- {
- int temp = 20;
- sleep (2);
- printf ("child2 中变量 temp 的地址为 0x%p\n", &temp);
- pthread_setspecific (key, &temp);
- printf ("child2 中 pthread_getspecific(key)返回的指针为:0x%p\n", (int *)pthread_getspecific(key));
- printf ("利用 pthread_getspecific(key)打印 child2 线程中与key关联的整型变量temp 值:%d\n", *((int *)pthread_getspecific(key)));
- }
-
- int main (void)
- {
- pthread_t tid1, tid2;
-
- pthread_key_create (&key, NULL);
-
- pthread_create (&tid1, NULL, (void *)child1, NULL);
- pthread_create (&tid2, NULL, (void *)child2, NULL);
- pthread_join (tid1, NULL);
- pthread_join (tid2, NULL);
-
- pthread_key_delete (key);
-
- return (0);
- }
运行结果:
- huangcheng@ubuntu:~$ ./a.out
- 结构体struct_data的地址为 0x0xb77db388
- child1 中 pthread_getspecific(key)返回的指针为:0x0xb77db388
- 利用 pthread_getspecific(key)打印 child1 线程中与key关联的结构体中成员值:
- struct_data.i:10
- struct_data.k: 3.141500
- ------------------------------------------------------
- child2 中变量 temp 的地址为 0x0xb6fda38c
- child2 中 pthread_getspecific(key)返回的指针为:0x0xb6fda38c
- 利用 pthread_getspecific(key)打印 child2 线程中与key关联的整型变量temp 值:20
取消键与线程私有数据之间的关联:
- int pthread_delete(pthread_key_t *keyp);
注意调用pthread_delete不会激活与键关联的析构函数。删除线程私有数据键的时候,不会影响任何线程对该键设置的线程私有数据值,甚至不影响调用线程当前键值,所以容易造成内存泄露(因为键不与私有数据关联了,当线程正常退出的时候不会调用键的析构函数,最终导致线程的私有数据这块内存没有释放)。使用已经删除的私有数据键将导致未定义的行为。
注意:对于每个pthread_key_t变量(即键)必须仅调用一次pthread_key_create。如果一个键创建两次,其实是在创建不同的键,第二个键将覆盖第一个,第一个键与任何线程可能为其设置的值将一起永远的丢失。所以,pthread_key_create放在主函数中执行;或每个线程使用pthread_once来创建键。
线程私有数据与键关联:
- int pthread_setspecific(pthread_key_t key,const void *value);
- void* pthread_getspecific(pthread_key_t key);
如果没有线程私有数据值与键关联,pthread_getspecific键返回NULL,可以依据此来确定是否调用pthread_setspecific。
注意:两个线程对自己的私有数据操作是互相不影响的。也就是说,虽然 key 是同名且全局,但访问的内存空间并不是相同的一个。key 就像是一个数据管理员,线程的私有数据只是到他那去注册,让它知道你这个数据的存在。
区别和联系:
1、进程是独立运行的实体,有独立的资源分配;
2、同一进程的线程之间共享进程的资源;
3、所有的进程至少有一个执行线程;
4、线程的创建和切换代价比进程的小;
线程间的通信方法:
1、同一进程的线程之间通信的最简单办法就是使用全局变量;
2、不同进程的线程之间通信需要通过下面进程间的通信来实现;
进程间的通信方法:
1、管道
2、信号量
3、共享内存
4、消息队列
5、套接字
传统意义上, 进程之间是不共享地址空间的, 而线程是共享着进程的地址空间.
健壮性
由于多个线程共享同一个进程的地址空间和相关的资源, 所以当一个线程出现crash,那么可能会导致相应的地址空间和资源 会出现问题,从而导致其它的线程也crash. 这个也很好理解,一个简单的大家可能都经历过的就是IE7吧, 当一个tab突然崩溃时,所有的tab都会崩溃,这时通常IE要重启(重启进程,重新生成线程).
而多进程则不存在这个问题, 因为不同的地址空间和资源, 当一个进程崩溃时, 并不会影响到其它进程. 同样,如果你用过chrome,如果一个tab崩溃了(chrome那搞笑的提示信息), 我们只需要关掉这个tab即可,并 不会影响到其它的tab.
性能
进程的安全性,健壮性是建立在独立的地址空间和独立的资源的条件下的, 所以进程的 启动, 关闭, 切换 相比于线程会有更多的开销. 而这种开销的差别在Windows下更加显著, 请参考下面 特定操作系统的进程和线程 部分的详细说明.
Windows
通常做过Windows多任务系统开发的程序员肯定会知道, Windows中的进程比线程有很大的开销, 要一定坚持使用线程, 那么为什么呢?
这里有个讨论可供参考: Why is creating a new process more expensive on Windows than Linux?
简单总结下, 原因:
- 这是Windows的设计的理念所致(多用户和并行的要求不高的特性)
- 在创建进程时,会有相当的系统调用
究竟有哪些额外的系统调用,请参考上面帖子.
Linux
让我们回到本文的摘要部分的引入, 我的室友提出的对于我而言 闻所未闻 的新观点.
那么, 在Linux下 进程和线程真的没有本质区别吗?
首先大家可以参考这个帖子, Threads vs Processes in Linux.
下面内容摘自 Threads vs Processes in Linux.
Linux uses a 1-1 threading model, with (to the kernel) no distinction between processes and threads
-- everything is simply a runnable task. *
On Linux, the system call clone clones a task, with a configurable level of sharing, among which are:
CLONE_FILES: share the same file descriptor table (instead of creating a copy)
CLONE_PARENT: don't set up a parent-child relationship between the new task and the old
(otherwise, child's getppid() = parent's getpid())
CLONE_VM: share the same memory space (instead of creating a COW copy)
fork() calls clone(least sharing) and pthread_create() calls clone(most sharing). **
forking costs a tiny bit more than pthread_createing because of copying tables and creating COW mappings for memory,
but the Linux kernel developers have tried (and succeeded) at minimizing those costs.
Switching between tasks, if they share the same memory space and various tables, will be a tiny bit cheaper
than if they aren't shared, because the data may already be loaded in cache. However,
switching tasks is still very fast even if nothing is shared -- this is something else that Linux kernel developers
try to ensure (and succeed at ensuring).
In fact, if you are on a multi-processor system, not sharing may actually be a performance boon:
if each task is running on a different processor, synchronizing shared memory is expensive.
上面其实已经讲得很清楚了,
- 对于内核而言, 进程和线程是 没有区别的
- 在用户的角度而言,区别在于如何创建(clone), 如果使用是 least shared ,那么就类似于进程的创建(最少共享)
- 如果使用的是 most sharing 那么就类似于线程的创建(最多共享)
- 由于Linux内核开发人员的努力和优化, 创建, 切换, 关闭 进程和线程之前的开销差异已经十分的小了