前言
本文将详细介绍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);
}