聊聊Linux中的线程本地存储——什么是TLS
Posted 海枫
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了聊聊Linux中的线程本地存储——什么是TLS相关的知识,希望对你有一定的参考价值。
从本篇开始进入另一个话题:线程本地存储(Thread Local Storage),在介绍这个概念前先说说变量和多线程的相关知识。
多线程下的变量模型
在单线程模型下,变量定义有两个维度,那就是在何处定义,以及它的修饰属性(static, extern,auto,register等)。extern属性表示声明一个变量 ,与定义无关,在此不作讨论;而register是将变量优化成寄存器里面,不作讨论。与变量定义相关的修饰属性就只有auto和static了。这两个维度可以得到变量的类型,以及它们的行为。评价一个变量的行为,通过常是从可见性和
生命周期两方面评定的,下面将变量的“何处定义”和“修饰属性”,得出“变量类型”以及从“可见性”和“生命周期”列出一个完整的表格,如下:
何处定义 | 修饰属性 | 变量类型 | 生命周期 | 可见性 |
---|---|---|---|---|
在函数外 | auto | 全局变量 | 整个程序 | 全局可见 |
在函数外 | static | 全局静态变量 | 整个程序 | 同一.c文件可见 |
在函数内 | auto | 局部变量 | 函数开始执行到结束这段时间 | 函数内可见 |
在函数内 | static | 静态局部变量 | 整个程序 | 函数内可见 |
对于静态局部变量会有这样的疑问,它只能在函数内使用,为什么它的生命周期是全局的?其实它与局部变量是不一样的,局部变量在函数开始执行时,才创建出来的,在函数执行结束时,它就消失了,不存在了。而静态局部变量,在函数没有执行时,它就已经在存在了,这就是为什么说它是整个程序的生命周期。当函数执行结束之后,它仍然存在,下次再执行该函数时,它是基于之前执行后的结果,开始执行的。所以它的行为与全局变量是一样的,不同之外是只能在函数内才能对它进行访问。
在单线程编程下面,这一切都协调得很好。因为整个进程只有一个执行单元,何任时刻只有一个执行代码(或者函数)在访问同一变量。
然而在多线程模式下,这一切都发生了变化。在多线程模式下面,同时出现多个执行单元,也即同一个函数会同时在多个执行单元中运行
提起多线程,几乎都想起应用加锁进行保护数据,但这不是充分必要的。
局量变量(生命周期在函数内的)是不受多线程影响,因为每个线程栈是独立的,多个线程同时执行该函数,访问局变部变量都是每线程一份的,不会产生数据竞争(race)。
但对于生命周期为整个程序的全局变量、全局静态变量和静态局部变量就不是那么一回事了。多线程下出现多个执行单元同时访问该变量,会出现数据竞争。在这种情况下,必须使用加锁进行数据保护,才能保证程序逻辑的正确性。
提起加锁,直觉反应就是将部分代码又回到串行执行的时代,如果加锁很频繁,那整个进程能并发执行的代码比例并不高,使用多线程带来的加速比并不高,最重要的是无法根据处理器个数来动态伸缩。
在多程线程序设计中,很多时候对数据进行划分,不同的数据区域交给不同的线程处理,可以避免多线程中竞争访问数据。但是有一个问题得考虑,那就是:各个独立线程在执行函数时(通常是不同线程执行相同的函数,只是他们访问的数据不同),这些函数的中间输出结果能否已是线程独立的呢?如果能完全独立,则多线程之间完全不需要互斥,伸缩性最优。
这个问题有两个解决办法,首先想到的是这些函数的输入和输出全部用参数传递。但它对编程要求很高, 有时这样的设计往往并不是最优的。很多时候函数的中间输出结果需要使用全局变量来保存(当然我们不推荐全局变量满天飞,但在一个模块内的静态全局变量还是常有的),这样就需要加锁保护,效率低下。
在这个场景下,每个线程都希望自己看到的全局变量是自己的,它也不想看到别人的,也不想别人对它有修改,它也不想修改别人的,全局变量也是个形式而已,最终目的是它可以独立拥有一份。
那么要解决多线程下的高效编程问题,必须对原来的变量模型稍作修改,支持一个额外的属性,那就是变量是多执行单元(线程)共享还是独立拥有一份。
何处定义 | 修饰属性 | 多执行单元共享or独享 | 变量类型 | 生命周期 | 可见性 |
---|---|---|---|---|---|
在函数外 | auto | 共享 | 全局变量 | 整个程序 | 全局可见 |
在函数外 | auto | 独享 | 全局线程变量 | 整个程序 | 全局可见 |
在函数外 | static | 共享 | 全局静态变量 | 整个程序 | 同一.c文件可见 |
在函数外 | static | 独享 | 全局静态线程变量 | 整个程序 | 同一.c文件可见 |
在函数内 | auto | 独享 | 局部变量 | 只在函数开始执行到结束这段时间 | 函数内 |
在函数内 | static | 共享 | 静态局部变量 | 整个程序 | 函数内可见 |
在函数内 | static | 独享 | 静态局部线程变量 | 整个程序 | 函数内可见 |
注意,局部变量本来就是线程函数内独享的,所以它没有共享这个属性值,整个程序生命周期的变量都有共享和独享这两个属性值。
什么是TLS
写到这里,TLS的定义就不言而喻。TLS全称为Thread Local Storage,即线程本地存储。在单线程模式下,所有整个程序生命周期的变量都是只有一份,那是因为只是一个执行单元;而在多线程模式下,有些变量需要支持每个线程独享一份的功能。这种每线程独享的变量放到每个线程专有的存储区域,所以称为线程本地存储(Thread Local Storage)或者线程私有数据(Thread Specific Data)。
Linux下支持两种方式定义和使用TLS变量,具体如下表:
定义方式 | 支持层次 | 访问方式 |
---|---|---|
__thread关键字 | 语言层面 | 与全局变量完全一样 |
pthread_key_create函数 | 运行库层面 | pthread_get_specific和pthread_set_specific对线程变量进行读写 |
应用语言支持的__thread关键字是最简单的,只须在定义变量时增加一个__thread关键字,后续对该变量的访问方式完全保持不变,所以这个是语言级别上的支持,属于隐式支持。_thread关键字是gcc对C语言的扩展,不是C语言标准定义的,当然Windows下的Visual Stdio也使用另一个关键字做扩展。
__thread例子
下面使用简单的例子说明__thread关键字的用法,以及实际的效果。
__thread int var = 0; // var定义为线程变量,每个数线拥有一份
void* worker(void* arg);
int main()
pthread_t pid1,pid2;
pthread_create(&pid1,NULL,worker,(void *)0);
pthread_create(&pid2,NULL,worker,(void *)1);
pthread_join(pid1,NULL);
pthread_join(pid2,NULL);
return 0;
void* worker(void* arg)
int idx = (int)arg;
int i;
for (i = 0; i < 100; ++i)
printf("thread:%d ++var = %d\\n", idx, ++var); // 每个线程只访问自己独享的那份
使用pthread_key_create/pthread_get_specific/pthread_set_specific函数创建和使用的线程变量,在后面的会作相应的介绍,我们暂时不作讨论。
后面文章中,我们使用线程变量这一术语表示每线程独享一份的变量,而使用TLS述语来表示对这一类变量存储属性的统称。
以上是关于聊聊Linux中的线程本地存储——什么是TLS的主要内容,如果未能解决你的问题,请参考以下文章