文章目录
对线程的理解
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,**只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈)**,但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
线程所独享的资源有:
程序计数器、寄存器、栈、状态字
- 程序计数器: 一个 CPU 在某个时间点,只能做一件事情,在多线程的情况下,CPU 运行时间被划分成若干个时间片,分配给各个线程执行;****程序计数器的作用就是记录当前线程执行的位置, 当线程被切换回来的时候,能够找到该线程上次运行到哪儿了;所以程序计数器一定是线程隔离的。
- 状态字状态字用于表示CPU执行指令时所具有的状态。 一些指令是否执行或以何方式执行可能取决于状态字中的某些位;执行指令时也可能改变状态字中的某些位,也能在位逻辑指令或字逻辑指令中访问并检测他们。
线程为什么需要私有数据
原因一:有时候需要维护基于每个线程的数据, 用线程ID作为索引。因为线程ID不能保证是小而连续的整数,所以不能简单的分配一个线程数据数组,用线程ID作为数组的索引。即使线程ID确实是小而连续的整数,可能还希望有一些额外的保护,以防止某个线程的数据和其它线程的数据相混淆。
原因二:可以让基于进程的接口适应多线程环境,比如errno,线程出现以前errno被定义成进程环境中全局可访问的整数,线程出现以后,为了让线程也能使用那些原本基于进程的系统调用和库例程,errno被重新定义成线程私有数据。
线程的私有数据实现的原理
线程私有数据实现的主要思想是: 在分配线程私有数据之前,创建与该数据相关联的键,这个键可以被进程中的所有线程使用,但每个线程把这个键与不同的线程私有数据地址进行关联,需要说明的是每个系统支持有限数量的线程特定数据元素(下面的例子以128个为限制)。
键的实现原理
其实系统为每个进程维护了一个称之为Key结构的结构数组,如下图所示:
在上图中Key 结构的 “标志”指示这个数据元素是否正在使用 。在刚开始时所有的标志初始化为“不在使用”。当一个线程调用pthread_key_create创建一个新的线程特定数据元素时,系统会搜索Key结构数组,找出第一个“不在使用”的元素。并把该元素的索引(0~127)称为“键”。 返回给调用线程的正是这个索引。
除了进程范围内的Key结构数组之外,系统还在进程内维护了关于多个线程的多条信息。这些特定于线程的信息我们称之为Pthread结构。 其中部分内容是我们称之为pkey数组的一个128个元素的指针数组。系统维护的关于每个线程的信息结构图如下:
在上图中,pkey数组所有元素都被初始化为空指针。这些128个指针是和进程内128个可能的键逐一关联的值。
API讲解
- pthread_key_t
/* Keys for thread-specific data */typedefunsignedint pthread_key_t;
- pthread_key_t无论是哪一个线程创建,其他所有的线程都是可见的,即一个进程中只需phread_key_create()一次
- 看似是全局变量,然而全局的只是key值,对于不同的线程对应的value值是不同的(通过pthread_setspcific和pthread_getspecific设置)
- pthread_key_create() 调用pthread_key_create()函数时系统首先会返回给我们一个Key结构数组中第一个“未被使用”的键(即索引值),每个线程可以随后通过该键找到对应的位置,并且为这个位置存储一个值(指针)。 一般来说,这个指针通常是每个线程通过调用malloc来获得的。
#include<pthread.h>/*
* 功能: 分配用于标识进程中线程特定数据的键。
* 参数: key 在分配( malloc )线程私有数据之前,需要创建和线程私有数据相关联的键( key ),这个键的功能是获得对线程私有数据的访问权
* destructor 清理函数名字( 如:fun )。当线程退出时,如果线程私有数据地址不是非 NULL,此函数会自动被调用。该函数指针可以设成 NULL ,这样系统将调用默认的清理函数。
* 返回值: 成功0,失败非0
*/intpthread_key_create(pthread_key_t *key,void(*destructor)(void*))
- 不论哪个线程调用 pthread_key_create(),所创建的 key 都是所有线程可访问,但各个线程可根据自己的需要往 key 中填入不同的值,相当于提供了一个同名不同值的变量。
- 因为进程中的所有线程都可以使用返回的键,所以key应该指向一个全局变量
- destructor指向一个自定义函数。只要线程终止时与key的关联值不为NULL,Pthreads API会自动执行解构函数,并将与key的关联值作为参数传入解构函数。如果无需解构,可以将destructor设置为NULL(系统将调用默认的清理函数)
- 如果一个线程有多个线程特有数据块,那么对各个解构函数的调用顺序是不固定的,每个解构函数的设计应该相互独立
- pthread_setspecific() 函数 pthread_setspecific()要求 Pthreads API 将 value 的副本存储于一数据结构中,并将 value 与调用线程以及 key 相关联(key 由之前对pthread_key_create()的调用返回)。
/*
功能: 设置线程私有数据( key ) 和 value 关联,注意,是 value 的值(不是所指的内容)和 key 相关联。
参数: key: 线程私有数据。
value: 指向由调用者分配的一块内存。 当线程终止时,会将该指针作为参数传递给与key对应的解构函数
参数 value也可以不是一个指向内存区域的指针,而是任何可以赋值(通过强制转换)
给 void*的标量值。在这种情况下,先前对 pthread_key_create()函数的调用应将 destructor
指定为 NULL。
返回值: 成功0,失败非0
*/intpthread_setspecific(pthread_key_t key,constvoid*value);
- pthread_getspecific()
pthread_getspecific()函数执行的操作与pthread_setspecific相反,返回之前与本线程及给定 key 相关的值(value)
取出所存储的值
- pthread_key_delete
/*
* 功能: 注销线程私有数据。这个函数并不会检查当前是否有线程正使用线程私有数据( key ),也不会调用清理函数 destructor() ,而只是将线程私有数据( key )释放以供下一次调用 pthread_key_create() 使用。
* 参数: 待注销的私有数据
* 返回值: 成功0,失败非0
*/intpthread_key_delete(pthread_key_t __key) __THROW
注意:
- 由于系统对每个进程中pthread_key_t类型的个数是有限制的,所以进程中并不能创建无限个的pthread_key_t变量。
- Linux中可以通过PTHREAD_KEY_MAX(定义于limits.h文件中)或者系统调用sysconf(_SC_THREAD_KEYS_MAX)来确定当前系统最多支持多少个键。Linux中默认是1024个键,这对于大多数程序来说已经足够了。
- 如果一个线程中有多个线程局部存储变量,通常可以将这些变量封装到一个数据结构中,然后使封装后的数据结构与一个线程局部变量相关联,这样就能减少对键值的使用。
API的使用步骤
- 创建一个连续为pthread_ket_t类型的变量
- 使用pthread_key_create()来创建该变量。
- 当线程中需要存储特殊值的时候,可以调用pthread_setspecific
- 如果需要取出所存储的值,调用pthread_getspecific
编程中使用线程的特定数据的过程
假设一个进程被启动,并且多个线程被创建。 其中一个线程调用pthread_key_create。系统在Key结构数组(图1- -第一幅图)中找到第1个未使用的元素。并把它的索引(0~127)返回给调用者。我们假设找到的索引为1 (我们会使用pthread_once 函数确保pthread_key_create只被调用一次)。
之后线程调用pthread_getspecific获取本线程的pkey[1] 的值(图(2)- -第二幅图中键1所值的指针), 返回值是一个空值,线程那么调用malloc分配内存区并初始化此内存区。 之后线程调用pthread_setspecific把对应的所创建键的线程特定数据指针(pkey[1]) 设置为指向它刚刚分配的内存区。下图指出了此时的情形。
当一个线程终止时,系统将扫描该线程的pkey数组,为每个非空的pkey指针调用相应的析构函数。 相应的析构函数是存放在图1中的Key数组中的函数指针。这是一个线程终止时其线程特定数据的释放手段。
练习范例
#include<stdio.h>#include<pthread.h>#include<unistd.h>
pthread_key_t key;voidechomsg(int t){printf("destructor excuted in thread %d,param=%d\n",pthread_self(),t);}void*child1(void*arg){
pthread_t tid=pthread_self();int i=1;printf("thread %d enter 此乃线程1\n",tid);pthread_setspecific(key,(void*)i);sleep(2);printf("thread %d returns %d 此乃线程1\n",tid,pthread_getspecific(key));sleep(5);}void*child2(void*arg){
pthread_t tid=pthread_self();int i=20;printf("thread %d enter 此乃线程2\n",tid);pthread_setspecific(key,(void*)i);sleep(1);printf("thread %d returns %d 此乃线程2\n",tid,pthread_getspecific(key));sleep(5);}intmain(void){
pthread_t tid1,tid2;printf("this is main thread\n");pthread_key_create(&key,(void(*)(void*))echomsg);pthread_create(&tid1,NULL,child1,NULL);pthread_create(&tid2,NULL,child2,NULL);sleep(10);pthread_key_delete(key);printf("main thread exit\n");return0;}
从运行结果来看,各线程对自己的私有数据操作互不影响。也就是说,虽然 key 是同名且全局,但访问的内存空间并不是同一个。
推荐一个零声学院免费公开课程,个人觉得老师讲得不错,
分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,
fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,
TCP/IP,协程,DPDK等技术内容,点击立即学习:服务器课程
版权归原作者 恒者走天下 所有, 如有侵权,请联系我们删除。