多线程安全编程规范:从踩坑到规范
多线程编程,说白了就是让多个工人同时干活。但工人多了,抢工具、抢材料的问题就来了。我见过太多项目,功能跑得好好的,一上多线程就崩。嗯,今天我们就聊聊怎么把多线程写稳。
核心原则:多线程安全不是靠运气,是靠规范。我个人的经验是,规范越早定,后期填坑越少。
一、编码规范:从源头避免问题
编码规范这东西,很多人觉得是形式主义。但我在项目中遇到过几次线上事故,追根溯源都是编码习惯不好。你想想看,一个锁没加对位置,可能让整个服务卡死。
1.1 变量声明规范
- 全局变量必须加锁:所有被多线程访问的全局变量,必须用互斥锁保护
- 尽量用局部变量:能放栈上的就别放堆上,能放线程局部存储的就别放全局
- 使用原子类型:对于简单的计数器、标志位,用
atomic_int替代普通 int
// 错误示范:普通全局变量
int counter = 0;
// 正确示范:原子变量
atomic_int counter = 0;
// 或者加锁保护
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int counter = 0;
void increment() {
pthread_mutex_lock(&lock);
counter++;
pthread_mutex_unlock(&lock);
}
1.2 锁的使用规范
我习惯给每个锁起个有意义的名字,别用 lock1、lock2 这种。为什么呢?因为代码审查的时候,别人根本不知道你在保护什么。
| 锁命名规范 | 说明 |
|---|---|
queue_lock |
保护队列的锁 |
cache_lock |
保护缓存的锁 |
user_table_lock |
保护用户表的锁 |
警告:锁的粒度要适中。太粗了性能差,太细了容易死锁。我见过有人给每个元素都加锁,结果性能比单线程还差。
二、代码审查要点:帮别人也是帮自己
代码审查不是找茬,是互相学习。我每次审查多线程代码,都会重点看这几个地方:
2.1 审查清单
- 锁的配对:lock 和 unlock 是否成对出现?有没有异常路径忘记解锁?
- 死锁风险:多个锁的加锁顺序是否一致?
- 竞态条件:检查 if-check 和 action 之间是否有窗口期
- 内存序:使用原子操作时,内存序是否正确?
// 常见竞态条件:检查-执行模式
if (queue->size > 0) { // 检查
// 这里可能被其他线程修改
item = queue_pop(queue); // 执行
}
// 正确做法:把检查和执行放在同一个锁内
pthread_mutex_lock(&queue_lock);
if (queue->size > 0) {
item = queue_pop(queue);
}
pthread_mutex_unlock(&queue_lock);
2.2 审查流程
我个人建议的审查流程是这样的:先看锁的覆盖范围,再看锁的粒度,最后看异常处理。我曾经因为漏看一个 goto 语句前的 unlock,导致线上服务挂了半小时。
三、常见陷阱总结:那些年我们踩过的坑
多线程的坑,说多不多,说少不少。但每个坑都能让你加班到深夜。我整理了几个最常见的:
3.1 陷阱一:虚假唤醒
条件变量的 wait 可能在没有被 notify 的情况下返回。这不是 bug,是 POSIX 标准允许的。所以 wait 之后必须重新检查条件。
// 错误:直接 wait
pthread_cond_wait(&cond, &lock);
// 假设条件已经满足
// 正确:用 while 循环检查
while (!condition) {
pthread_cond_wait(&cond, &lock);
}
3.2 陷阱二:忘记 volatile
编译器可能会优化掉看似无用的变量读取。如果一个变量被多个线程修改,记得加 volatile 或者用原子操作。
小技巧:我一般直接用 atomic_int 或 atomic_flag,省心。volatile 只能防止编译器优化,不能保证内存可见性。
3.3 陷阱三:锁的顺序不一致
两个线程分别持有锁 A 和锁 B,然后互相等待对方释放锁。这就是经典的死锁。解决办法很简单:所有线程按同样的顺序加锁。
// 线程1
lock(A);
lock(B);
// 干活
unlock(B);
unlock(A);
// 线程2(错误:顺序不同)
lock(B);
lock(A); // 可能死锁
// 干活
unlock(A);
unlock(B);
// 线程2(正确:顺序一致)
lock(A);
lock(B);
// 干活
unlock(B);
unlock(A);
四、最佳实践:让多线程代码更健壮
说了这么多坑,来点实用的。这些是我多年积累下来的经验:
4.1 封装锁操作
别到处写 lock/unlock。封装成函数或者宏,减少出错机会。
// 封装成宏
#define LOCK(m) pthread_mutex_lock(&(m))
#define UNLOCK(m) pthread_mutex_unlock(&(m))
// 或者用 RAII 风格(C 语言模拟)
typedef struct {
pthread_mutex_t *lock;
} LockGuard;
LockGuard lock_guard_init(pthread_mutex_t *lock) {
pthread_mutex_lock(lock);
return (LockGuard){.lock = lock};
}
void lock_guard_destroy(LockGuard *g) {
pthread_mutex_unlock(g->lock);
}
4.2 使用线程安全的数据结构
别自己造轮子。能用现成的线程安全队列、哈希表就用现成的。我见过太多人自己实现一个队列,结果各种 bug。
4.3 测试策略
- 压力测试:用比生产环境更多的线程跑
- 随机延迟:在关键路径上插入随机 sleep,暴露竞态条件
- 静态分析:用 ThreadSanitizer 等工具检测数据竞争
我的经验:多线程 bug 往往在低负载时隐藏得很好,一上高并发就暴露。所以测试时一定要模拟极端情况。
五、知识体系总览
下面这张图总结了多线程安全编程的核心要点。你可以把它当作 checklist,写代码时对照着看。
这张图把今天讲的内容串起来了。编码规范是基础,代码审查是保障,了解常见陷阱能帮你少走弯路,最佳实践则是让你写出更优雅的代码。
最后说一句:多线程编程没有银弹。规范、审查、测试,一个都不能少。我见过太多人觉得"我代码没问题",结果一上线就崩。保持敬畏心,多写测试,多 review,这才是正道。
公众号:蓝海资料掘金营,微信deep3321