聊聊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的主要内容,如果未能解决你的问题,请参考以下文章

PE线程本地存储

Envoy源码分析之ThreadLocal

Envoy源码分析之ThreadLocal

详谈内存管理技术线程模型

TLS 线程本地存储

javase线程怎么存储到容器