3. 线程同步之互斥锁
多线程编程里,最让人头疼的问题是什么?
我个人觉得,就是「数据打架」。
两个线程同时写一个变量,结果谁也不知道最终值是多少。
嗯,这就是竞态条件。解决它,最基础的工具就是互斥锁。
3.1 互斥锁的原理
互斥锁,说白了就是一把「门锁」。
线程想进临界区,得先拿到钥匙。
拿到钥匙的线程进去干活,其他线程只能在门外等着。
等里面的线程出来,把钥匙一扔,下一个线程才能进去。
这个机制,在操作系统层面叫「原子操作」。
锁的获取和释放,必须是不可分割的。
你想想看,如果两个线程同时检查锁状态,都以为锁是空闲的,那就乱套了。
我在项目中遇到过一个问题:
一个日志模块,多个线程同时写文件。
没加锁的时候,日志文件里经常出现乱码,甚至两行日志混在一起。
加了互斥锁之后,问题立刻消失。
核心要点:
- 互斥锁保证同一时刻只有一个线程进入临界区
- 锁的获取和释放是原子操作
- 其他线程在锁释放前会被阻塞
3.2 pthread_mutex_t 的使用
在 POSIX 线程库中,互斥锁的类型是 pthread_mutex_t。
使用起来其实很简单,就四个步骤:
- 定义锁变量
- 初始化锁
- 加锁
- 解锁
- 销毁锁
来看一个完整的例子:
#include <pthread.h>
#include <stdio.h>
pthread_mutex_t lock;
int shared_data = 0;
void* worker(void* arg) {
for (int i = 0; i < 100000; i++) {
pthread_mutex_lock(&lock);
shared_data++;
pthread_mutex_unlock(&lock);
}
return NULL;
}
int main() {
pthread_t t1, t2;
// 初始化互斥锁
pthread_mutex_init(&lock, NULL);
pthread_create(&t1, NULL, worker, NULL);
pthread_create(&t2, NULL, worker, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("最终结果: %d\n", shared_data);
// 销毁锁
pthread_mutex_destroy(&lock);
return 0;
}
这段代码里,shared_data++ 就是临界区。
如果不加锁,两个线程同时执行 shared_data++,结果肯定小于 200000。
加了锁,结果永远是 200000。
我的习惯:
初始化锁的时候,我一般用 PTHREAD_MUTEX_INITIALIZER 宏,省去显式调用 pthread_mutex_init。
但如果是动态分配的锁,还是得用 pthread_mutex_init。
3.3 死锁的产生与预防
死锁,是互斥锁使用中最容易踩的坑。
我曾经在一个网络服务器项目里,调试了整整两天,才发现是死锁问题。
两个线程各自持有一把锁,又都在等对方释放锁,结果谁也动不了。
死锁产生的四个必要条件:
| 条件 | 说明 |
|---|---|
| 互斥 | 资源一次只能被一个线程占用 |
| 持有并等待 | 线程持有资源的同时,还在等待其他资源 |
| 不可剥夺 | 资源只能由持有者主动释放 |
| 循环等待 | 多个线程形成等待环路 |
预防死锁,我总结了三条经验:
- 固定锁顺序:所有线程都按相同的顺序加锁。比如先锁 A,再锁 B,不要有的线程先锁 B 再锁 A。
- 使用 trylock:用
pthread_mutex_trylock尝试加锁,如果失败就释放已有锁,避免死等。 - 缩小锁范围:只在真正需要保护的数据上加锁,不要锁住整个函数。
避坑指南:
我曾经在回调函数里加锁,结果回调函数又被另一个锁保护,形成了嵌套锁。
这种场景下,一定要用 pthread_mutexattr_settype 设置递归锁,否则会死锁。
3.4 锁的粒度控制
锁的粒度,说白了就是锁保护的数据范围。
粒度太粗,并发性能差;粒度太细,容易死锁,而且锁开销大。
我举个例子:
一个银行账户系统,有存款、取款、查询三个操作。
如果整个系统只用一把大锁,那查询操作也得等存款操作完成,显然不合理。
合理的做法是:
- 每个账户一把锁(细粒度)
- 或者按操作类型分锁(中等粒度)
- 或者整个系统一把锁(粗粒度,适合低并发场景)
选择锁的粒度,要看实际场景:
| 粒度 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 粗粒度 | 实现简单,不易死锁 | 并发性能差 | 低并发、数据量小 |
| 中等粒度 | 性能与复杂度平衡 | 需要仔细设计 | 大多数业务系统 |
| 细粒度 | 并发性能高 | 容易死锁,实现复杂 | 高并发、大数据量 |
我的建议:
刚开始写多线程程序时,先用粗粒度锁。
等程序跑通了,再用性能分析工具看看瓶颈在哪。
不要一开始就追求细粒度,那是给自己挖坑。
下面这张图,展示了互斥锁的核心知识体系:
嗯,互斥锁的内容就这些。
说白了,它就是多线程编程的「基本功」。
用好了,程序稳定高效;用不好,各种诡异问题等着你。
我个人觉得,理解互斥锁的关键在于:
想清楚「哪些数据需要保护」和「保护多久」。
这两个问题想明白了,锁就用对了。