0.引言
在现代的C++多线程编程中,数据竞争和线程安全一直是核心问题。为了更好地去解决此类问题,C++11标准引入了thread_local关键字,它允许我们创建每个线程独立的副本变量,从而简化了线程局部存储的使用。本文将对thread_local的使用方式、实现原理、运行开销进行分析,协助开发者系统的理解thread_local。
1.thread_local作用以及用法
thread_local用来定义每个线程独立存储实例的变量,每个线程对这些变量都有其自己的副本而不和其他线程共享。这就可以确保线程间的数据隔离,防止数据竞争,我们可以通过下面例子来看其用法:
#include <thread>
#include <stdio.h>
thread_local int local_var = 0;
void thread_function() {
printf("%x
", &local_var);
}
int main() {
std::thread t1(thread_function);
std::thread t2(thread_function);
t1.join();
t2.join();
return 0;
}
执行程序,其地址如下,也就是说每个线程的local_val对应不同的变量:

2.底层实现原理
上一节我们了解了thread_local的作用以及用法,那么这种隔离是怎么实现的那?我们从编译期和运行期两个部分来进行分析:
2.1 编译期的处理
第一在编译时会将对应符号添加标记,同时会生成特殊的指令,不会直接访问内存地址,而是通过重定位的方式来进行访问,这样就为不同线程的访问留下了空间,以linux为例,其生成了如下代码,通过%fs间接访问:

接下来我们可以来看目标文件的结构,有TLS变量的文件会生成两个特殊的段:
1).tdata: 用于存放已初始化的TLS变量;
2).tbss: 用于存放未初始化(或初始化为0)的TLS变量(类似于 .bss 段)。
由于我们的变量初始值为0,所以用了tbss段:

最后在连接时就可以进行整合并生成对应的访问语句,列如”mov %fs:0x10, %rax”,读取位于 %fs 寄存器值 + 0x10 字节处的数据。
2.2 运行期处理
运行期的处理我们从主线程启动和线程创建两个过程来看(以linux为例),下面的内存块是通过TCB(Thread Control Block)管理:
1)主线程启动时的创建:此时会为主线程分配TLS内存块,并将值拷贝进去,同时将fs寄存器设置为此TLS块的起始地址;
2)动态创建的TLS分配:当调用pthread_create创建新线程时会为新线程分配一个新的TLS内存,将数据拷贝进去作为初始值,然后设置对应的fs寄存器值为这个地址;
3)线程切换:由于线程切换涉及到上下文切换,此时会变更寄存器值,也就通过fs寄存器实现了动态访问不同地址。
3.性能开销分析
3.1 内存开销
从上面运行期的分析我们可以看到,其每个线程都需要一个TLS存储块,线程越多,总内存消耗线性增长。
3.2 时间开销
时间开销我们可以从两个方面来看:
1)访问开销:比访问普通全局变量多了一层寄存器间接寻址;
2)线程创建开销:多了构造新的TLS的时间开销。
4.使用提议
thread_local是C++提供的一个强劲而便捷的工具,它通过语言层面的支持,极大地简化了线程局部存储的使用。不过,这种便利性并非没有代价。其背后基于线程控制块和多重间接寻址的实现机制,带来了一些的访问开销和线程管理开销。
在大多数情况下,它的开销是可接受的;但在性能至上的场景下,可以借鉴其思想去寻找其他的解决方案(列如线程对象成员变量)。














- 最新
- 最热
只看作者