线程安全:线程安全函数、线程局部存储(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 有两种方式:

  1. POSIX 线程 API: pthread_key_create(), pthread_setspecific(), pthread_getspecific()
  2. 编译器关键字: __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() 这个名字有误导性,它其实不是杀死线程,而是给线程发信号。慎用。

五、知识体系总览

下面这张图,把本章的核心逻辑串起来了。你可以把它当作一个思维导图来用。

线程安全 线程安全函数 使用 _r 后缀版本 避免静态/全局数据 线程局部存储 TLS __thread 关键字 pthread_key_create API 线程与信号 pthread_sigmask 设置掩码 专用线程 + sigwait 核心:数据隔离 + 避免共享状态

你看,线程安全的核心就两条路:要么让函数本身不依赖共享数据(线程安全函数),要么让数据本身不共享(TLS)。至于信号,它是个异类,最好用专门的线程去伺候它。

好了,这一章的内容就到这里。记住我的一句话:写多线程代码时,默认所有东西都不是线程安全的。保持这个警惕心,能帮你避开 90% 的坑。


公众号:蓝海资料掘金营,微信deep3321