线程安全:线程安全函数、线程局部存储(TLS)、__thread关键字、线程与信号
各位同学,今天我们来聊聊多线程编程里一个绕不开的话题——线程安全。说实话,我早年刚接触多线程时,觉得这东西不就是加个锁嘛,有什么难的?结果被坑了几次之后,才明白这里面的门道远比想象中深。
线程安全,说白了就是:多个线程同时访问同一份数据时,程序的行为仍然是正确的。嗯,这句话听起来简单,但实现起来,处处是坑。
一、线程安全函数 vs 不可重入函数
先问大家一个问题:strtok() 这个函数,能在多线程里用吗?
答案是不行。为什么?因为它内部维护了一个静态指针,用来记住上次分割的位置。两个线程同时调用,这个静态变量就被乱改了。
我当年在一个网络协议解析模块里,就因为这个函数吃了大亏。线上偶发性的解析错误,查了两天才定位到是 strtok() 导致的。后来全部替换成了 strtok_r(),问题立刻消失。
所以,C 标准库里的函数,大致可以分为两类:
| 类别 | 特点 | 例子 |
|---|---|---|
| 线程安全函数 | 不依赖全局/静态数据,或已内部加锁 | strtok_r(), localtime_r(), rand_r() |
| 非线程安全函数 | 使用内部静态缓冲区或全局变量 | strtok(), localtime(), rand() |
大多数非线程安全函数,都有一个带 _r 后缀的线程安全版本。我个人习惯是:只要写多线程代码,一律用 _r 版本,省心。
核心原则:如果一个函数内部使用了静态变量、全局变量,或者返回了指向内部缓冲区的指针,那它大概率不是线程安全的。
二、线程局部存储(TLS)
有时候,我们希望每个线程都有自己的私有数据副本。比如每个线程需要一个独立的错误码变量,或者一个独立的日志缓冲区。
这时候,TLS(Thread Local Storage)就派上用场了。它让每个线程都拥有一份独立的变量副本,互不干扰。
实现 TLS 有两种方式:
- POSIX 线程 API:
pthread_key_create(),pthread_setspecific(),pthread_getspecific() - 编译器关键字:
__thread(GCC/Clang)
我个人更推荐第二种,因为代码更简洁,性能也更好。API 方式虽然灵活,但每次访问都要调用函数,开销不小。
三、__thread 关键字
这个关键字用起来非常简单。在全局变量或静态变量的声明前加上 __thread,编译器就会为每个线程生成独立的副本。
// 每个线程都有自己的 errno 副本
__thread int tls_errno = 0;
void set_error(int code) {
tls_errno = code;
}
int get_error() {
return tls_errno;
}
你看,就这么简单。每个线程调用 set_error() 和 get_error(),操作的都是自己的那份数据,完全不需要加锁。
使用注意:
__thread只能修饰 POD 类型(基本类型、指针、简单结构体)。不能修饰带有构造/析构函数的 C++ 类对象。- 不能修饰函数的局部变量(自动变量)。
- 初始化必须是编译期常量。
我曾经在一个高并发网关项目里,用 __thread 给每个线程分配了一个内存池。每个线程从自己的池里分配内存,完全无锁。性能直接翻了一倍。嗯,这就是 TLS 的魅力。
四、线程与信号
信号和多线程,这俩东西凑在一起,就是个麻烦事。我建议你记住一句话:尽量避免在多线程程序里使用信号。
为什么?因为信号的默认行为是发给进程,而不是某个特定线程。哪个线程处理?不确定。这就导致了不可预测性。
但有时候你又不得不用,比如 SIGINT 处理 Ctrl+C,或者 SIGSEGV 处理段错误。那怎么办?
几个关键点:
- 信号掩码: 每个线程都有自己的信号掩码。用
pthread_sigmask()可以设置。我建议在主线程里把所有信号都阻塞掉,然后专门开一个信号处理线程,用sigwait()来统一处理。 - 信号处理器: 信号处理器里能调用的函数非常有限。只能调用异步信号安全的函数(比如
write(),但不能调用printf())。 - 不要用信号做线程间通信: 有更好的机制——条件变量、消息队列、管道。信号太粗糙了。
避坑指南: 我曾经在一个项目中,用 pthread_kill() 给某个线程发信号,想让它退出。结果信号被另一个线程截获了,导致逻辑混乱。后来全部改成了原子变量 + 条件变量的方式,才彻底稳定下来。
记住:pthread_kill() 这个名字有误导性,它其实不是杀死线程,而是给线程发信号。慎用。
五、知识体系总览
下面这张图,把本章的核心逻辑串起来了。你可以把它当作一个思维导图来用。
你看,线程安全的核心就两条路:要么让函数本身不依赖共享数据(线程安全函数),要么让数据本身不共享(TLS)。至于信号,它是个异类,最好用专门的线程去伺候它。
好了,这一章的内容就到这里。记住我的一句话:写多线程代码时,默认所有东西都不是线程安全的。保持这个警惕心,能帮你避开 90% 的坑。