前言

本文将详细介绍pthread_key的用法以及pthread_key的原理。pthread_key在《ntyco协程》中,以及后续文章《try catch的实现》都有用到。跟我一起学习的读者务必搞懂原理。

本专栏知识点是通过零声教育的线上课学习,进行梳理总结写下文章,对c/c++linux课程感兴趣的读者,可以点击链接 C/C++后台高级服务器课程介绍 详细查看课程的服务。

线程私有数据

TSD概念

在多线程的程序中,所有的线程都可以使用和修改定义在全局的全局变量。也就是说全局变量被所有的线程共有。有没有一种办法,使得这个"全局变量",被线程独有,在线程的内部,该“全局变量”可以被线程的各个函数接口访问,但是对其他线程屏蔽呢?换句话说,本文后续要介绍的,就是线程私有数据,即 表面看起来是一个“全局变量”,所有的线程都可以使用它,但是每个线程都是独享它的,它的值在每一个线程中都是单独存储的。

线程私有数据(TSD):同名而不同值,即同key不同value,一键多值,所以对私有数据的访问都是通过键来访问到value的。

Posix api细节探究

Posix定义了两个API来创建和销毁TSD,以及两个API来设置与访问TSD

/* Functions for handling thread-specific data.  */

/* Create a key value identifying a location in the thread-specific
   data area.  Each thread maintains a distinct thread-specific data
   area.  DESTR_FUNCTION, if non-NULL, is called with the value
   associated to that key when the key is destroyed.
   DESTR_FUNCTION is not called if the value associated is NULL when
   the key is destroyed.  */
extern int pthread_key_create (pthread_key_t *__key,
			       void (*__destr_function) (void *))
     __THROW __nonnull ((1));

/* Destroy KEY.  */
extern int pthread_key_delete (pthread_key_t __key) __THROW;

/* Return current value of the thread-specific data slot identified by KEY.  */
extern void *pthread_getspecific (pthread_key_t __key) __THROW;

/* Store POINTER in the thread-specific data slot identified by KEY. */
extern int pthread_setspecific (pthread_key_t __key,
				const void *__pointer) __THROW ;

pthread_key_create:创建一个键

int pthread_key_create(pthread_key_t *key, void (*destr_function) (void*));

首先从Linux的TSD池中分配一项,然后将其值赋给key供以后访问使用。接口的第一个参数是指向参数的指针,第二参数是函数指针,如果该指针不为空,那么在线程执行完毕退出时,已key指向的内容为入参调用destr_function(),释放分配的缓冲区以及其他数据。

前面已经说了,key是全局变量,不论哪个线程调用了pthread_key_create,所创建的key都是所有的线程都可以访问,每个线程根据自己的需求往key中set不同的值,这就形成了同名而不同值,即同key不同value,一键多值。

在Linux中,TSD池用一个结构体数组来表示,并且PTHREAD_KEYS_MAX默认为1024

cat /usr/include/bits/local_lim.h


static struct pthread_key_struct pthread_keys[PTHREAD_KEYS_MAX] ={{0,NULL}};

struct pthread_key_struct
{
  /* Sequence numbers.  Even numbers indicated vacant entries.  Note
     that zero is even.  We use uintptr_t to not require padding on
     32- and 64-bit machines.  On 64-bit machines it helps to avoid
     wrapping, too.  */
  uintptr_t seq;

  /* Destructor for the data.  */
  void (*destr) (void *);
};

创建一个TSD,相当于将结构体数组的某一个元素的seq值设置为为“in_use”,并将其索引返回给 *key,然后设置destr_function()为destr()。pthread_key_create()创建一个新的线程私有数据key时,系统会搜索进程中的这个key数组,找出一个未使用的将其索引赋值给 *key。

pthread_key_delete:注销一个键

int pthread_key_delete (pthread_key_t __key);

这个函数不会检查当前是否有线程正在使用TSD,也不会调用清理函数destr_function,而是将TSD对应的seq置un_use,并且将相关线程对应的value置为NULL,以供下一次调用pthread_key_create使用。

pthread_setspecific:为指定key 设置线程私有数据 val

int pthread_setspecific(pthread_key_t key, const void *pointer);

该接口将指针pointer的值(指针值而非其指向的内容)与key相关联,用pthread_setspecific为一个键指定新的线程数据时,线程必须释放原有的数据用以回收空间。

在Linux线程中,使用一个位于 线程描述结构体(_pthread_descr_struct) 中的void **p_specific[PTHREAD_KEY_1STLEVEL_SIZE];指针数组来存放与key关联的数据val。因为PTHREAD_KEYS_MAX 为1024,所以一维数组大小为32

#define PTHREAD_KEY_2NDLEVEL_SIZE 32
#define PTHREAD_KEY_1STLEVEL_SIZE \
((PTHREAD_KEYS_MAX + PTHREAD_KEY_2NDLEVEL_SIZE -1) \
/ PTHREAD_KEY_2NDLEVEL_SIZE 

所以具体存放的位置由key值经过计算得到

idx1st = key/PTHREAD_KEY_2NDLEVEL_SIZE 
idx2nd = key%PTHREAD_KEY_2NDLEVEL_SIZE 

pthread_getspecific:从指定键读取线程的私有数据

void * pthread_getspecific(pthread_key_t key);

读函数就是将与key关联的数据val读出来,数据类型为void * ,所有可以指向任何类型的数据。通过上面我们得知,数据存在一个32 * 32的二维数组中,所以访问的时候,也是通过计算key值得到数据的位置再返回其内容的

一图诠释底层数据结构的关联

跑个demo看看效果

该demo开了3个线程,第一个线程key对应着一个int的整数,第二个线程key对应着字符串,第三个线程key对应着一个结构体。发现都能正常打印,并且在线程执行完毕退出时,已key指向的内容为入参调用destr_function(),释放分配的缓冲区以及其他数据。

//
// Created by 68725 on 2022/7/29.
//

#include<pthread.h>
#include<stdio.h>
#include <malloc.h>
#include <memory.h>

#define THREAD_COUNT 3

pthread_key_t key;

typedef void *(*thread_cb)(void *);

void print_thread1_key(void) {
    int *p = (int *) pthread_getspecific(key);//将值从私有空间中取出来
    printf("thread 1 : %d\n", *p);
}

//线程1 的回调函数
void *thread1_proc(void *arg) {
    int *p = (int *) malloc(sizeof(int));
    *p = 68725032;
    pthread_setspecific(key, p);//将 i传入私有空间中
    print_thread1_key();
}

void print_thread2_key(void) {
    char *ptr = (char *) pthread_getspecific(key);
    printf("thread 2 : %s\n", ptr);
}

//线程2 的回调函数
void *thread2_proc(void *arg) {
    char *ptr = (char *) malloc(1024 * sizeof(char));
    strcpy(ptr, "wxfnb");
    pthread_setspecific(key, ptr);
    print_thread2_key();
}


struct pair {
    int x;
    int y;
};


void print_thread3_key(void) {
    struct pair *p = (struct pair *) pthread_getspecific(key);
    printf("thread 3  x: %d, y: %d\n", p->x, p->y);
}

//线程3 的回调函数
void *thread3_proc(void *arg) {
    struct pair *p = (struct pair *) malloc(sizeof(struct pair));
    p->x = 1;
    p->y = 2;
    pthread_setspecific(key, p);
    print_thread3_key();
}


void destroy_func(void *val) {
    printf("free key\n");
    free(val);
}

int main() {
    pthread_t th_id[THREAD_COUNT] = {0};//3个线程id
    pthread_key_create(&key, destroy_func);//创建key,这个全局变量可以认为是线程内部的私有空间

    thread_cb callback[THREAD_COUNT] = {//线程的回调函数
            thread1_proc,
            thread2_proc,
            thread3_proc
    };

    int i;
    for (i = 0; i < THREAD_COUNT; i++) {//创建线程
        pthread_create(&th_id[i], NULL, callback[i], NULL);
    }

    for (i = 0; i < THREAD_COUNT; i++) {
        pthread_join(th_id[i], NULL);//主线程需要等待子线程执行完成之后再结束
    }

    pthread_key_delete(key);
}