线程的私有存储空间


对线程的理解

线程是进程的一个实体,是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讲解

  1. pthread_key_t
/* Keys for thread-specific data */
typedef unsigned int pthread_key_t;
  • pthread_key_t无论是哪一个线程创建,其他所有的线程都是可见的,即一个进程中只需phread_key_create()一次
  • 看似是全局变量,然而全局的只是key值,对于不同的线程对应的value值是不同的(通过pthread_setspcific和pthread_getspecific设置)
  1. pthread_key_create()
    调用pthread_key_create()函数时系统首先会返回给我们一个Key结构数组中第一个“未被使用”的键(即索引值),每个线程可以随后通过该键找到对应的位置,并且为这个位置存储一个值(指针)。 一般来说,这个指针通常是每个线程通过调用malloc来获得的。
#include <pthread.h>
/*
* 功能:   分配用于标识进程中线程特定数据的键。
* 参数:   key       在分配( malloc )线程私有数据之前,需要创建和线程私有数据相关联的键( key ),这个键的功能是获得对线程私有数据的访问权
*         destructor  清理函数名字( 如:fun )。当线程退出时,如果线程私有数据地址不是非 NULL,此函数会自动被调用。该函数指针可以设成 NULL ,这样系统将调用默认的清理函数。
* 返回值: 成功0,失败非0
*/
int pthread_key_create (pthread_key_t *key,
			       void (*destructor) (void *))

  • 不论哪个线程调用 pthread_key_create(),所创建的 key 都是所有线程可访问,但各个线程可根据自己的需要往 key 中填入不同的值,相当于提供了一个同名不同值的变量

  • 因为进程中的所有线程都可以使用返回的键,所以key应该指向一个全局变量

  • destructor指向一个自定义函数。只要线程终止时与key的关联值不为NULL,Pthreads API会自动执行解构函数,并将与key的关联值作为参数传入解构函数。如果无需解构,可以将destructor设置为NULL(系统将调用默认的清理函数)

  • 如果一个线程有多个线程特有数据块,那么对各个解构函数的调用顺序是不固定的,每个解构函数的设计应该相互独立

  1. 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
	*/
int pthread_setspecific(pthread_key_t key, const void *value);



  1. pthread_getspecific()

pthread_getspecific()函数执行的操作与pthread_setspecific相反,返回之前与本线程及给定 key 相关的值(value)
取出所存储的值

  1. pthread_key_delete
/*
* 功能: 注销线程私有数据。这个函数并不会检查当前是否有线程正使用线程私有数据( key ),也不会调用清理函数 destructor() ,而只是将线程私有数据( key )释放以供下一次调用 pthread_key_create() 使用。
* 参数:  待注销的私有数据
* 返回值: 成功0,失败非0
*/
int pthread_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;
void echomsg(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);
}
int main(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");
        return 0;
}

在这里插入图片描述
从运行结果来看,各线程对自己的私有数据操作互不影响。也就是说,虽然 key 是同名且全局,但访问的内存空间并不是同一个。

推荐一个零声学院免费公开课程,个人觉得老师讲得不错,
分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,
fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,
TCP/IP,协程,DPDK等技术内容,点击立即学习:服务器课程

  • 11
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值