8、线程局部存储:__thread关键字的使用、线程局部存储的原理、TLS与全局变量的对比、实际应用场景
说到多线程编程,有个问题一直挺让人头疼——数据共享。全局变量吧,所有线程都能看到,得加锁。局部变量吧,又只在函数内部有效。那有没有一种变量,每个线程都有自己的独立副本,互不干扰?
有的。这就是线程局部存储(Thread Local Storage,简称TLS)。
我个人习惯把TLS叫做「线程的私有全局变量」。听起来有点矛盾?其实不矛盾。它作用域是全局的,但每个线程访问的是自己的那份拷贝。嗯,就像每个人都有自己的水杯,虽然都叫「水杯」,但里面的水各喝各的。
8.1 __thread关键字的使用
在C语言里,实现TLS最直接的方式就是用__thread关键字。这是GCC和Clang都支持的扩展,C11标准里也有_Thread_local,用法差不多。
先看个最简单的例子:
#include <stdio.h>
#include <pthread.h>
__thread int tls_var = 0;
void* thread_func(void* arg) {
tls_var = (int)(long)arg;
printf("线程 %ld: tls_var = %d\n", (long)arg, tls_var);
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_create(&t1, NULL, thread_func, (void*)1);
pthread_create(&t2, NULL, thread_func, (void*)2);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("主线程: tls_var = %d\n", tls_var);
return 0;
}
运行结果:
线程 1: tls_var = 1
线程 2: tls_var = 2
主线程: tls_var = 0
看到了吗?三个线程各自操作tls_var,互不影响。主线程的值还是0,线程1是1,线程2是2。这就是TLS的魔力。
__thread只能修饰全局变量或静态局部变量,不能修饰普通局部变量- 只能修饰POD类型(基本类型、指针、结构体等),不能修饰有构造函数的C++对象
- 声明时最好初始化,虽然不初始化也会默认清零,但显式初始化更安全
8.2 线程局部存储的原理
你可能会问:这玩意儿底层是怎么实现的?
说白了,每个线程在创建时,系统会为它分配一块独立的内存区域,专门存放TLS变量。这块区域通常叫「线程控制块」(TCB)或者「线程本地存储段」。
我画个图帮你理解:
实现方式主要有两种:
- 静态TLS:编译时就确定每个线程需要的TLS变量大小,分配固定偏移。访问速度快,但不够灵活。
- 动态TLS:运行时通过系统调用(如
pthread_key_create)动态分配。灵活,但每次访问都要查表,略慢。
__thread属于静态TLS。编译器在生成代码时,会把TLS变量的访问翻译成「基址+偏移量」的形式。基址是线程控制块的地址,偏移量是编译时确定的。所以访问速度几乎和普通全局变量一样快。
关键点:每个线程都有自己的栈空间,TLS变量就存放在线程的私有数据区里。线程切换时,TLS的基址寄存器也会跟着切换。这就是为什么不同线程访问同一个TLS变量名,却得到不同值的原因。
8.3 TLS与全局变量的对比
我整理了一张对比表,方便你直观理解:
| 对比维度 | 全局变量 | TLS变量 |
|---|---|---|
| 存储位置 | 数据段(.data/.bss) | 线程私有存储区 |
| 可见性 | 所有线程共享 | 每个线程独享 |
| 线程安全 | 不安全,需要加锁 | 天然安全,无需加锁 |
| 访问速度 | 最快(直接寻址) | 略慢(需要间接寻址) |
| 内存占用 | 固定一份 | 每线程一份,N线程占用N倍 |
| 初始化时机 | 程序启动时 | 线程创建时 |
| 适用场景 | 共享状态、配置信息 | 线程上下文、缓存数据 |
举个例子你就明白了。假设你写一个日志库,每个线程需要记录自己的日志缓冲区。用全局变量的话,所有线程往同一个缓冲区写,得加锁,性能差。用TLS的话,每个线程写自己的缓冲区,最后再合并,完全不用锁。
8.4 实际应用场景
TLS在实际项目中用得挺多的。我挑几个典型的说说:
场景一:errno变量
你肯定用过errno吧?它就是TLS的经典案例。每个线程都有自己的errno,线程A调用函数出错设置errno = EINVAL,不会影响线程B的errno。如果errno是普通全局变量,多线程下根本没法用。
场景二:线程私有缓存
比如数据库连接池,每个线程可以缓存自己的连接。用TLS存储连接句柄,避免每次查询都去连接池取,减少锁竞争。
__thread DBConnection* tls_db_conn = NULL;
DBConnection* get_db_connection() {
if (tls_db_conn == NULL) {
tls_db_conn = create_new_connection();
}
return tls_db_conn;
}
场景三:日志系统
高性能日志库常用TLS做缓冲区。每个线程先把日志写到自己的缓冲区,满了再统一刷到文件。这样避免了全局锁,写入速度能提升好几倍。
场景四:随机数生成器
rand()函数内部有全局状态,多线程调用会有问题。用TLS给每个线程维护一个独立的随机数种子,既安全又高效。
__thread unsigned int tls_seed = 0;
int thread_safe_rand() {
if (tls_seed == 0) {
tls_seed = time(NULL) ^ pthread_self();
}
return rand_r(&tls_seed);
}
好了,关于线程局部存储就聊这么多。说白了,TLS就是给每个线程配了个「私人储物柜」,放什么自己说了算,别人碰不着。用好了,能省掉不少锁的麻烦。
公众号:蓝海资料掘金营,微信deep3321