多线程安全编程规范:从踩坑到规范

多线程编程,说白了就是让多个工人同时干活。但工人多了,抢工具、抢材料的问题就来了。我见过太多项目,功能跑得好好的,一上多线程就崩。嗯,今天我们就聊聊怎么把多线程写稳。

核心原则:多线程安全不是靠运气,是靠规范。我个人的经验是,规范越早定,后期填坑越少。

一、编码规范:从源头避免问题

编码规范这东西,很多人觉得是形式主义。但我在项目中遇到过几次线上事故,追根溯源都是编码习惯不好。你想想看,一个锁没加对位置,可能让整个服务卡死。

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 锁的使用规范

我习惯给每个锁起个有意义的名字,别用 lock1lock2 这种。为什么呢?因为代码审查的时候,别人根本不知道你在保护什么。

锁命名规范 说明
queue_lock 保护队列的锁
cache_lock 保护缓存的锁
user_table_lock 保护用户表的锁

警告:锁的粒度要适中。太粗了性能差,太细了容易死锁。我见过有人给每个元素都加锁,结果性能比单线程还差。

二、代码审查要点:帮别人也是帮自己

代码审查不是找茬,是互相学习。我每次审查多线程代码,都会重点看这几个地方:

2.1 审查清单

  1. 锁的配对:lock 和 unlock 是否成对出现?有没有异常路径忘记解锁?
  2. 死锁风险:多个锁的加锁顺序是否一致?
  3. 竞态条件:检查 if-check 和 action 之间是否有窗口期
  4. 内存序:使用原子操作时,内存序是否正确?
// 常见竞态条件:检查-执行模式
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_intatomic_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,写代码时对照着看。

多线程安全编程 编码规范 代码审查要点 常见陷阱 最佳实践 变量声明 锁的使用 锁配对 死锁检查 虚假唤醒 volatile 封装锁操作 安全数据结构 压力测试 多线程安全编程知识体系

这张图把今天讲的内容串起来了。编码规范是基础,代码审查是保障,了解常见陷阱能帮你少走弯路,最佳实践则是让你写出更优雅的代码。

最后说一句:多线程编程没有银弹。规范、审查、测试,一个都不能少。我见过太多人觉得"我代码没问题",结果一上线就崩。保持敬畏心,多写测试,多 review,这才是正道。


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