系统为每一个线程提供了一个私有“容器”,该容器中保存的是一个个的键值对。通过键可以获取值,也可以向此容器中保存值。pthread 中,对键的类型和值的类型都是有要求的。
1. 键类型及相关函数
键的类型是 pthread_key_t,它只能通过函数 pthread_key_create 进行初始化。它的定义如下:
int pthread_key_create(pthread_key_t *key, void (*destructor(void*));
键可被任意一个线程使用。
注意到上面的函数还有第二个参数,它需要传递一个析构函数,比如像下面这样:
void destructor(void* arg) {
...
}
当线程运行结束(return 或 pthread_exit)时,该函数会自动调用。析构函数的参数,是与该键关联的值(value).
pthread_key_create 第二个参数可以为空。
通过 pthread_key_delete 函数,可能删除指定的键:
int pthread_key_delete(pthread_key_t key);
注意,调用上面的函数,不会引起前面的析构函数的调用。
2. 保存键值对
线程容器中的值类型必须是 void*。可以通过下面的函数将键值对保存到线程自己的容器中。
int pthread_setspecific(pthread_key_t key, const void *value);
不同线程调用上面的函数时,只会将键值对保存到自己(指线程自己)的容器中。
所以,我们把这种键值对称为线程私有数据。
3. 根据键来获取值
可以通过下面的函数根据键来获取对应的值:
void* pthread_getspecific(pthread_key_t key);
注意,获取到的值类型仍然是 void* 类型。
4. 程序清单
程序 specdata 创建了两个线程,它们各自向自己的容器中保存了不同的数据,然后又从容器中将数据提取出来,并打印。
所有线程结束的时候,会调用 destructor 析构函数。
4.1 代码
// specdata.c
#include <stdio.h>
#include <pthread.h>
pthread_key_t key;
void destructor(void *arg) {
printf("destructor: hello %d\n", (int)arg);
}
void *fun1(void *arg) {
int data = 5;
pthread_setspecific(key, (void*)data);
int x = (int)pthread_getspecific(key);
printf("fun1: x = %d\n", x);
}
void *fun2(void *arg) {
int data = 10;
pthread_setspecific(key, (void*)data);
int x = (int)pthread_getspecific(key);
printf("fun2: x = %d\n", x);
}
int main() {
pthread_t tid1, tid2;
// 初始化 key
pthread_key_create(&key, destructor);
pthread_create(&tid1, NULL, fun1, NULL);
pthread_create(&tid2, NULL, fun2, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
4.2 编译和运行
- 编译和运行
$ gcc specdata.c -o specdata -lpthread
$ ./specdata
- 运行结果
图1 运行结果
4.3 结果分析
从图 1 可以看到,线程 fun1 和 fun2 中各自都向其容器中保存了不同的数据,然后又根据 key 将其提取出来并打印。线程运行结束后,会调用 destructor 函数,该函数的参数就是容器中的值。
实际上,如果一个线程容器中有多个键值对,当线程结束时,会多次调用 destructor 函数,调用的顺序和你初始化(pthread_key_create)键的顺序是一致的。
5. 总结
- 知道线程私有变量是什么(线程私有容器中的键值对)
- 掌握保存和获取键值对的方法
练习:在《只被执行一次的函数》一文中,你已经学会了在线程中调用初始化函数的方法,请你把本文实验中的键初始化流程改为使用 pthread_once 方法进行初始化。