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相关联,指针指向的内存是线程特定数据。

32-线程控制——线程特定数据

具体的过程如上图所示:
   当启动一个进程并创建了若干线程,其中一个线程要申请线程特定数据,调用了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;
}

程序执行结果:
32-线程控制——线程特定数据

   程序创建了线程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需要注意两点:

  1. 定义一个pthread_once_t类型的全局控制变量,必须使用PTHREAD_ONCE_INIT宏来进行初始化。
  2. 还需要定义一个初始化函数,比如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;
}

执行结果:
32-线程控制——线程特定数据

从程序执行结果来看,只有最后一个线程调用了初始化函数,且只执行了一次。


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;
}

执行结果:
32-线程控制——线程特定数据


7. 线程特定数据操作函数注意事项


  1. 如果线程中调用了exit,_exit等函数,或出现了非正常退出,则不会调用析构函数。

  2. 如果线程的特定数据使用了malloc分配内存,需要在析构函数中释放内存,否则将出现内存泄漏