• Linux线程私有数据Thread-specific Data(TSD) 详解


    前言

      本文将详细介绍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 ;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    pthread_key_create:创建一个键

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

      首先从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
    
    • 1
    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 *);
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

      创建一个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);
    
    • 1

      这个函数不会检查当前是否有线程正在使用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);
    
    • 1

      该接口将指针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 
    
    • 1
    • 2
    • 3
    • 4

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

    idx1st = key/PTHREAD_KEY_2NDLEVEL_SIZE 
    idx2nd = key%PTHREAD_KEY_2NDLEVEL_SIZE 
    
    • 1
    • 2

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

    void * pthread_getspecific(pthread_key_t key);
    
    • 1

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

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

    在这里插入图片描述

    跑个demo看看效果

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

    //
    // Created by 68725 on 2022/7/29.
    //
    
    #include
    #include
    #include 
    #include 
    
    #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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
  • 相关阅读:
    SQLserver-快速复制一行数据到数据库并修改ID
    零基础学python之列表
    uni-app 之 安装uView,安装scss/sass编译
    STM32F1与STM32CubeIDE编程实例-超声波测距传感器驱动
    linux内核中断进入过程分析笔记
    http2分片流内容整合呈现方法
    一文看懂25个神经网络模型,神经网络模型结构图
    安装docker ,更换docker版本
    算法与数据结构【30天】集训营——线性表的定义及特点-顺序表的表示与实现及操作(03)
    SpringMVC(四):SSM整合
  • 原文地址:https://blog.csdn.net/qq_42956653/article/details/126129532