73. 互斥锁:pthread_mutex_lock 的使用
多线程编程里,最让人头疼的问题是什么?
我个人觉得,不是线程怎么创建,也不是怎么回收,而是——数据打架。
两个线程同时写一个变量,你写一半我插一脚,结果数据全乱套了。这种问题在嵌入式开发中尤其致命。我在做车载项目时就遇到过,一个传感器数据采集线程和主控线程共享一个缓冲区,不加锁的时候,偶尔会出现采集到一半的数据被拿去计算了,结果算出来的值完全不对,车子差点报故障。
嗯,从那以后,我对互斥锁就格外上心了。
什么是互斥锁?
互斥锁,说白了就是一把「门锁」。一次只允许一个线程进去,其他线程在门外等着。等里面的线程出来了,下一个才能进去。
在 POSIX 线程库中,互斥锁的类型是 pthread_mutex_t。常用的操作就三个:
pthread_mutex_init— 初始化锁pthread_mutex_lock— 加锁pthread_mutex_unlock— 解锁
你想想看,如果两个线程同时调用 pthread_mutex_lock,只有一个能成功拿到锁,另一个会被阻塞住,直到锁被释放。
基本用法
先看一个最简单的例子。两个线程分别对一个全局变量做累加操作,如果不加锁,结果肯定不对。
#include <pthread.h>
#include <stdio.h>
int counter = 0;
pthread_mutex_t lock;
void* thread_func(void* arg) {
for (int i = 0; i < 100000; i++) {
pthread_mutex_lock(&lock);
counter++;
pthread_mutex_unlock(&lock);
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_mutex_init(&lock, NULL);
pthread_create(&t1, NULL, thread_func, NULL);
pthread_create(&t2, NULL, thread_func, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_mutex_destroy(&lock);
printf("counter = %d\n", counter);
return 0;
}
这段代码里,counter++ 这个操作被锁保护起来了。每次只有一个线程能执行它。最终结果一定是 200000,不会少。
核心要点:锁的范围要尽量小,只保护真正需要保护的临界区。锁太大了,性能会下降;锁太小了,又可能漏掉保护。
初始化方式
互斥锁有两种初始化方式:
| 方式 | 代码 | 说明 |
|---|---|---|
| 静态初始化 | pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; |
适用于全局或静态变量,不需要调用 init |
| 动态初始化 | pthread_mutex_init(&lock, NULL); |
适用于局部变量或需要设置属性的场景 |
我个人习惯用动态初始化,因为更灵活。比如你可以设置锁的属性为「递归锁」,允许同一个线程多次加锁而不死锁。不过一般情况下,默认属性就够了。
常见陷阱
用互斥锁的时候,有几个坑我踩过,你一定要注意:
陷阱一:忘记解锁
如果代码中某个分支提前 return 了,或者发生了异常,锁没有释放,其他线程就会永远等下去。这就是死锁。
我曾经在一个项目中,加锁后调用了某个库函数,那个函数在某种条件下会直接 exit,结果锁没释放,整个进程卡死了。后来我改成在加锁后先保存锁的状态,用 goto 统一处理退出点。
陷阱二:重复加锁
默认的互斥锁是「非递归」的。同一个线程如果连续两次调用 pthread_mutex_lock,第二次会把自己锁死。这就是所谓的「自死锁」。
解决办法:要么用递归锁,要么确保代码逻辑不会重复加锁。
小技巧:我写代码时,习惯把 lock 和 unlock 放在同一个函数层级,尽量不跨函数调用。如果实在要跨函数,我会在注释里明确标注「此函数调用者需持有锁」。
核心逻辑流程图
下面这张图展示了两个线程竞争互斥锁的完整流程:
从图中可以看到,线程1先拿到锁,线程2只能等着。等线程1解锁后,线程2才能拿到锁继续执行。这就是互斥锁最核心的「互斥」特性。
实际项目中的建议
我在嵌入式项目中总结了几条经验,分享给你:
- 锁的粒度要适中。不要锁住整个大函数,只锁住真正需要保护的几行代码。否则其他线程会被不必要地阻塞。
- 尽量使用静态初始化。对于全局锁,用
PTHREAD_MUTEX_INITIALIZER更简洁,也避免了忘记调用 init 的问题。 - 加锁和解锁要成对出现。我习惯在写 lock 之后立刻写 unlock,然后再在中间插入业务代码。这样不容易漏掉。
- 注意锁的顺序。如果多个线程需要多个锁,一定要约定好加锁顺序,否则容易死锁。比如线程1先锁A再锁B,线程2先锁B再锁A,就可能互相等待。
一句话总结:互斥锁是保护共享数据最基础的工具,用好了能避免绝大多数数据竞争问题。但用不好,死锁、性能下降、逻辑错误都会找上门来。
好了,关于 pthread_mutex_lock 的基本用法和注意事项就讲到这里。下一道题我们会聊聊读写锁,那又是另一种场景下的利器了。
公众号:蓝海资料掘金营,微信deep3321