48、线程安全:全局变量与静态变量的线程竞争
多线程编程里,全局变量和静态变量是最容易出问题的地方。
我见过太多人在这上面栽跟头了。说白了,就是多个线程同时读写同一块内存,结果数据乱掉了。
问题本质:共享资源的非原子操作
先看一个典型例子:
// 全局计数器
int counter = 0;
void* thread_func(void* arg) {
for (int i = 0; i < 100000; i++) {
counter++; // 这行有问题!
}
return NULL;
}
两个线程同时跑这个函数。你猜最终 counter 是多少?
不是 200000。可能只有 120000 左右。为什么?
counter++ 看起来是一条语句,但 CPU 执行时是三步:
- 从内存读 counter 到寄存器
- 寄存器加 1
- 写回内存
两个线程可能同时读到同一个值,比如 100。各自加 1,再写回 101。本该加 2 的,结果只加了 1。这就是经典的「读-改-写」竞争。
核心要点:只要多个线程同时访问同一块内存,且至少有一个是写操作,就必须加保护。
静态变量同样危险
很多人以为函数内部的 static 变量是安全的。其实不然。
int get_next_id() {
static int id = 0;
return id++; // 同样有竞争!
}
这个函数被多个线程调用时,返回的 ID 可能重复。我在项目中遇到过类似问题——一个日志系统用静态变量生成序列号,结果线上日志出现了重复 ID,排查了好久才发现是这里的问题。
常见保护手段
| 方法 | 适用场景 | 注意事项 |
|---|---|---|
| 互斥锁 (mutex) | 通用场景 | 注意死锁,锁粒度要小 |
| 读写锁 (rwlock) | 读多写少 | 写操作会阻塞所有读 |
| 原子操作 | 简单计数器、标志位 | 只能处理简单类型 |
| 线程局部存储 (TLS) | 每个线程独立的数据 | 不能跨线程共享 |
用互斥锁的正确姿势
#include <pthread.h>
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int counter = 0;
void* thread_func(void* arg) {
for (int i = 0; i < 100000; i++) {
pthread_mutex_lock(&lock);
counter++;
pthread_mutex_unlock(&lock);
}
return NULL;
}
嗯,这里要注意:锁的粒度。如果循环里每次都加锁解锁,性能会下降很多。我习惯的做法是:
void* thread_func(void* arg) {
int local_count = 0;
for (int i = 0; i < 100000; i++) {
local_count++; // 先算本地
}
pthread_mutex_lock(&lock);
counter += local_count; // 一次性合并
pthread_mutex_unlock(&lock);
return NULL;
}
这样锁只加一次,性能好得多。
原子操作:轻量级方案
C11 标准提供了原子类型。如果你的编译器支持,可以用:
#include <stdatomic.h>
atomic_int counter = 0;
void* thread_func(void* arg) {
for (int i = 0; i < 100000; i++) {
atomic_fetch_add(&counter, 1);
}
return NULL;
}
原子操作比锁轻量,但只能处理简单的加减、比较交换等操作。复杂逻辑还是得用锁。
我的建议:能用原子操作就别用锁。但别为了用原子操作而把逻辑搞复杂。代码可读性也很重要。
静态变量的另一种陷阱:初始化竞争
C++ 里有个坑:函数内静态变量的初始化不是线程安全的(C++11 之前)。
// 多个线程第一次调用时可能出问题
Config* get_config() {
static Config* cfg = new Config(); // 初始化有竞争!
return cfg;
}
C 语言里没有构造函数的问题,但静态变量的初始化同样要注意。我建议:
- 在 main 函数开始前,单线程完成所有静态变量的初始化
- 或者用 pthread_once 确保只初始化一次
避坑指南
我曾经踩过的坑:
- 以为只读的全局变量不需要保护——错!如果另一个线程在修改它,读也会出问题
- 忘记对 bool 类型加保护——bool 在有些平台不是原子操作
- 在信号处理函数里访问全局变量——信号和线程的交互更复杂
知识体系图
总结
全局变量和静态变量的线程竞争,说白了就是「多个线程同时动同一块内存」。解决思路就两条:要么不让它们同时动(加锁),要么让操作变成原子的。
我个人习惯是:能不用全局变量就不用。实在要用,就加锁保护。静态变量也一样——别因为它藏在函数里就觉得安全。
记住一句话:共享可变状态是万恶之源。减少共享,你的代码就会安全很多。
最后提醒:写完多线程代码后,用 ThreadSanitizer 跑一遍。它能帮你发现很多肉眼看不出的竞争问题。我每次提交代码前都会跑一遍,省了不少事。