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 互斥锁(空闲) pthread_mutex_lock 线程1 持有锁 pthread_mutex_lock 线程2 阻塞等待 互斥锁(空闲) pthread_mutex_unlock 线程2 获得锁 持有锁 阻塞等待 锁空闲

从图中可以看到,线程1先拿到锁,线程2只能等着。等线程1解锁后,线程2才能拿到锁继续执行。这就是互斥锁最核心的「互斥」特性。

实际项目中的建议

我在嵌入式项目中总结了几条经验,分享给你:

  • 锁的粒度要适中。不要锁住整个大函数,只锁住真正需要保护的几行代码。否则其他线程会被不必要地阻塞。
  • 尽量使用静态初始化。对于全局锁,用 PTHREAD_MUTEX_INITIALIZER 更简洁,也避免了忘记调用 init 的问题。
  • 加锁和解锁要成对出现。我习惯在写 lock 之后立刻写 unlock,然后再在中间插入业务代码。这样不容易漏掉。
  • 注意锁的顺序。如果多个线程需要多个锁,一定要约定好加锁顺序,否则容易死锁。比如线程1先锁A再锁B,线程2先锁B再锁A,就可能互相等待。

一句话总结:互斥锁是保护共享数据最基础的工具,用好了能避免绝大多数数据竞争问题。但用不好,死锁、性能下降、逻辑错误都会找上门来。

好了,关于 pthread_mutex_lock 的基本用法和注意事项就讲到这里。下一道题我们会聊聊读写锁,那又是另一种场景下的利器了。


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