32-线程控制——线程特定数据
1. 线程特定数据
线程特定数据(thread-specific-data),也称为线程私有数据(thread-private-data),是存储和查询某个特定线程相关数据的一种机制(APUE的说法)。
之所以称为线程私有数据,是因为让每个线程访问属于自己独有的数据,这样就不用关心线程同步的问题。比如线程id就是线程私有数据,为了防止与其他线程的数据混淆,需要进行一些保护。
但是一个进程中的所有线程几乎可以访问进程的整个地址空间,除了寄存器以外,一个线程是无法阻止其他线程访问它的数据,即便是特定数据也不行。但是通过管理线程特定数据的函数可以提高线程间数据的独立性,尽量做到阻止线程访问其他线程的数据。
2. 进程的key数组和线程的pthread结构
POSIX规定为每个进程维护了一个key结构的容器(可以理解为一个数组),通过这个数组可以获取或保存线程私有数据,这个数组中的每个结构称之为一个线程特定数据元素
,POSIX规定系统实现的Key结构数组必须包含不少于128个线程特定元素。
每个线程的线程特定数据元素至少包含两项内容:标志和析构函数指针
,key结构的的标志表示这个数组元素是否使用,析构函数指针表示线程退出时释放线程特定数据。
当一个线程调用pthread_key_create函数创建一个key时,系统就会从key数组中关联一个未使用的key。创建的key通过pthread_key_create函数的参数keyp指针返回该key的地址,第二个参数是一个函数指针,指向一个析构函数。
除了进程内key数组,系统还为进程内的每个线程维护了一个线程结构,也就是pthread结构。pthread结构维护的pkey指针是和进程的key数组的所有key相关联,指针指向的内存是线程特定数据。
具体的过程如上图所示:
当启动一个进程并创建了若干线程,其中一个线程要申请线程特定数据,调用了pthread_key_create函数在进程中的key结构数组
中找到一个未使用的元素,并把key返回给调用者,假设返回的这个key索引号为0,线程调用pthread_getspecific函数把本线程的pkey[0]值与进程的key数组的key[0]相关联,返回的是一个空指针pkey = NULL,而这个pkey指针就是指向实际的线程特定数据的首地址了
。
此时现在为空,需要通过malloc分配内存,在调用pthread_setspecific()调用将线程特定数据的指针指向刚分配的内存块,整个过程如上图所示。
3. 线程特定数据操作函数
在使用线程特定数据前,需要创建与特定数据关联的键,通过这个键可以访问线程特定数据,通过pthread_key_create函数进行创建一个键值,键的数据类型为pthread_key_t。
int pthread_key_create(pthread_key_t *key, void (*destructor(void*));
参数key:创建返回的键值
参数destructor:是一个函数指针,指向一个析构函数,线程退出时会调用该析构函数。
比如下面这个函数就是参数2,在线程退出时会自动被调用:
//析构函数
void destructor(void* arg) {
...
}
pthread_key_delete函数用于删除指定的键与线程特定数据之间的关联关系。
int pthread_key_delete(pthread_key_t key);
需要注意的是,调用pthread_key_delete不会引起前面的析构函数调用。
pthread_setpecific函数用于根据键值去设置(关联)线程特定数据。
int pthread_setspecific(pthread_key_t key, const void *value);
参数key:指定设置的键值
参数value:线程特定数据的内存首地址
该函数用于根据键值去获取线程特定数据
void *pthread_getspecific(pthread_key_t key);
返回值:成功返回线程特定数据,失败则该键值没有关联。
4. 线程特定数据实例
该程序创建两个线程,设置并获取各自的线程特定数据。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
//定义键值
pthread_key_t key;
//析构函数
void destructor(void *arg){
printf("destructor data = %d\n\n",(int)arg);
}
//线程1
void *tfn1(void *arg){
//线程特定数据
int data = 110;
//设置线程特定数据
int ret = pthread_setspecific(key , (void *)data);
if(ret != 0){
perror("pthread_setspecific");
pthread_exit(NULL);
}
//获取线程特定数据
int res = (int)pthread_getspecific(key);
printf("tid1: data = %d\n",res); //输出线程特定数据
printf("tid1 will going to die\n");
return (void *)0;
}
//线程2
void *tfn2(void *arg){
//线程特定数据
int data = 999;
//设置线程特定数据
int ret = pthread_setspecific(key , (void *)data);
if(ret != 0){
perror("pthread_setspecific");
pthread_exit(NULL);
}
//获取线程特定数据
int res = (int)pthread_getspecific(key);
printf("tid2: data = %d\n",res); //输出线程特定数据
printf("tid2 will going to die\n");
return (void *)0;
}
int main(void){
pthread_t tid1 , tid2;
int ret;
//关联键值
ret = pthread_key_create(&key , destructor);
if(ret != 0){
perror("pthread_key_create");
return -1;
}
//创建2个线程
ret = pthread_create(&tid1 , NULL , tfn1 , NULL);
if(ret != 0){
perror("pthread_create");
}
ret = pthread_create(&tid2 , NULL , tfn2 , NULL);
if(ret != 0){
perror("pthread_create");
}
pthread_join(tid1 , NULL);
pthread_join(tid2 , NULL);
return 0;
}
程序执行结果:
程序创建了线程1和线程2,线程主控函数设置了各自的线程特定数据,然后又获取了各自的数据,线程退出前打印going to ide提示,紧接着会调用析构函数释放线程的资源,且析构函数中的值就是线程的特定数据。
5. pthread_once机制
在前面的线程特定数据示例中,假设因为线程竞争的关系导致多次调用pthread_key_create函数的话,可能会出现一些不可预料的情况,比如有些线程可能看到一个key值,其他线程看到的可能是不同的key值。
为了避免出现这种情况,我们希望调用pthread_key_create函数初始化的时候只执行一次。
POSIX pthread提供了pthread_once,如果我们想要对某些数据只初始化一次就可以使用pthread_once了。
int phtread_once(pthread_once_t *initflag, void (*initfn)(void));
返回值:若成功返回0,若失败返回错误编号
参数initflag:指定控制变量(once_ocntrol),用于记录初始化函数的执行状态
参数initfn:是 一个函数指针,就是指定的初始化函数,函数类型为void (*initfn)(void)
使用pthread_once需要注意两点:
- 定义一个pthread_once_t类型的全局控制变量,必须使用PTHREAD_ONCE_INIT宏来进行初始化。
- 还需要定义一个初始化函数,比如pthread_init函数。
pthread_once函数首先会检查控制变量(once_control)的初值,判断是否已经完成初始化,如果完成就简单返回,否则pthread_once调用初始化函数,控制变量会记录初始化完成。如果已经有一个线程在初始化时,其他线程再调用pthread_once会阻塞等待,直到那个线程初始化完成才返回。
//once_control控制变量值必须指定为PTHREAD_ONCE_INIT
pthread_once_t once_control = PTHREAD_ONCE_INIT;
这样pthread_once指定的初始化函数只执行一次,而once_control控制变量表示初始化函数是否执行过,具体是哪个线程执行初始化函数这是不确定的。
实验代码:
创建6个线程调用pthread_once函数进行初始化。
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
//定义控制变量并设置初值
pthread_once_t once_control = PTHREAD_ONCE_INIT;
//共享资源初始化函数 ,只有一个线程执行
void pthread_once_init(void){
//打印初始化共享数据的线程id
printf("tid = %lu pthread shared data is init succes!\n", pthread_self());
}
//线程主控函数
void *tfn(void *arg){
//执行初始化函数
pthread_once(&once_control , pthread_once_init);
//打印信息
int num = (int)arg;
printf("I’m thread id = %d tid = %lu\n",num,pthread_self());
return (void *)0;
}
int main(void){
pthread_t tid[6];
int i;
int ret;
//创建6个线程
for(i = 0; i < 6; ++i){
ret = pthread_create(&tid[i] , NULL , tfn , (void *)i);
if(ret != 0){
printf("pthread_create tid[%d] error\n", i);
}
}
//回收线程
for(i = 0; i < 6; ++i){
pthread_join(tid[i] , NULL);
}
return 0;
}
执行结果:
从程序执行结果来看,只有最后一个线程调用了初始化函数,且只执行了一次。
6. 线程特定数据实例改进版
通过pthread_once机制对线程特定数据实例进行改写
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
//定义控制变量,初始化必须为PTHREAD_ONCE_INIT
pthread_once_t once_control = PTHREAD_ONCE_INIT;
//定义键值
pthread_key_t key;
//定义析构函数
void destructor(void *arg){
printf("destructor data = %d\n\n" , (int)arg);
}
//初始化函数
void pthread_init(void){
//创建,初始化(在此初始化)
printf("pthread_once is running\n");
pthread_key_create(&key , destructor);
}
void *tfn1(void *arg){
int data = 110;
pthread_setspecific(key , (void *)data);
int res = (int)pthread_getspecific(key);
printf("pthread1: data = %d\n" , res);
return (void *)0;
}
void *tfn2(void *arg){
int data = 120;
pthread_setspecific(key , (void *)data);
int res = (int)pthread_getspecific(key);
printf("pthread2: data = %d\n" , res);
return (void *)0;
}
int main(void){
pthread_t tid1 , tid2;
//调用pthread_once函数
pthread_once(&once_control , pthread_init);
pthread_create(&tid1 , NULL , tfn1 , NULL);
pthread_create(&tid2 , NULL , tfn2 , NULL);
pthread_join(tid1 , NULL);
pthread_join(tid2 , NULL);
return 0;
}
执行结果:
7. 线程特定数据操作函数注意事项
-
如果线程中调用了exit,_exit等函数,或出现了非正常退出,则不会调用析构函数。
-
如果线程的特定数据使用了malloc分配内存,需要在析构函数中释放内存,否则将出现内存泄漏