一、互斥锁(Mutex)的核心概念
1. 为什么需要互斥锁?
线程共享进程的所有资源(如全局变量、文件描述符),当多个线程同时操作共享资源 时,会出现 "竞态条件"(Race Condition)------ 数据被交替修改,导致结果错乱。
举个例子 :两个线程同时对全局变量 count 做 count++(看似简单的操作,实际是 "读→改→写" 三步):
- 线程 A 读
count=0→ 准备改成 1,但还没写回; - 线程 B 此时也读
count=0→ 改成 1 并写回; - 线程 A 再写回 1;
- 最终
count=1,但预期是 2,数据错乱。
互斥锁的作用就是保证同一时间只有一个线程能操作共享资源(即 "互斥"),把 "读→改→写" 变成原子操作(不可分割)。
2. 互斥锁的核心特性
- 互斥性:同一时刻,只有一个线程能持有锁,其他线程尝试加锁会阻塞;
- 原子性:加锁 / 解锁操作是原子的,不会被 CPU 调度打断;
- 非递归:同一个线程不能重复加锁(否则会死锁);
- 释放原则:只有持有锁的线程能解锁,其他线程解锁会报错。
3. 形象比喻
互斥锁 = 卫生间的门锁:
- 线程要使用卫生间(操作共享资源),必须先 "上锁"(加锁);
- 其他线程想进,只能在门口等(阻塞);
- 用完后必须 "解锁"(释放锁),否则其他线程永远进不去(死锁);
- 同一个人不能连续锁两次门(非递归)。
二、Linux 互斥锁核心函数(pthread 库)
Linux 互斥锁的函数都在 pthread 库中,编译时需加 -lpthread,核心类型是 pthread_mutex_t。
1. 互斥锁初始化:pthread_mutex_init()
功能:初始化一个互斥锁。
函数原型:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
参数解释:
mutex:指向 pthread_mutex_t 类型的指针,用于存储互斥锁。attr:指向 pthread_mutexattr_t 类型的指针,用于指定互斥锁属性。通常设置为 NULL 表示使用默认属性。- 返回值:成功返回 0,失败返回错误码。
2. 加锁:pthread_mutex_lock()(阻塞式)
功能 :尝试获取互斥锁 ,若锁已被其他线程持有,则当前线程阻塞(暂停运行),直到锁被释放。
函数原型:
int pthread_mutex_lock(pthread_mutex_t *mutex);
参数 :mutex:指向 pthread_mutex_t 类型的指针,用于获取互斥锁。
返回值:成功返回 0,失败返回错误码。
3. 尝试加锁:pthread_mutex_trylock()(非阻塞式)
功能 :尝试获取互斥锁,若锁已被持有,不阻塞 ,直接返回错误码(EBUSY),线程可继续执行其他逻辑。
函数原型:
int pthread_mutex_trylock(pthread_mutex_t *mutex);
参数:mutex:指向 pthread_mutex_t 类型的指针,用于尝试获取互斥锁。
适用场景:不想让线程阻塞,比如 "能拿到锁就操作资源,拿不到就先做别的事"。
4. 解锁:pthread_mutex_unlock()
功能 :释放持有的互斥锁,唤醒等待该锁的线程(由内核调度器选择一个线程获得锁)。
函数原型:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数:mutex:指向 pthread_mutex_t 类型的指针,用于释放互斥锁。
注意:
- 只有持有锁的线程 能解锁,其他线程解锁会返回
EPERM错误; - 解锁前必须确保已加锁,否则会导致未定义行为(如死锁)。
5. 销毁互斥锁:pthread_mutex_destroy()
功能:销毁互斥锁,释放相关资源(仅适用于动态初始化的锁)。
函数原型:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:mutex:指向 pthread_mutex_t 类型的指针,用于销毁互斥锁。
注意 :销毁前必须确保锁已解锁,且没有线程在等待该锁,否则会返回 EBUSY 错误。
三、互斥锁的常见问题与注意事项
1. 死锁(最常见问题)
死锁场景:
- 场景 1:同一个线程重复加锁(
pthread_mutex_lock调用两次); - 场景 2:线程 A 持有锁 1,等待锁 2;线程 B 持有锁 2,等待锁 1。
解决方法:
- 避免重复加锁;
- 统一锁的获取顺序(如先获取锁 1,再获取锁 2);
- 用
pthread_mutex_trylock替代阻塞加锁,超时则释放已持有的锁。
2. 锁的粒度
- 粗粒度锁:一个锁保护所有共享资源(简单,但并发效率低);
- 细粒度锁:多个锁分别保护不同的共享资源(并发效率高,但易出死锁);
- 建议:根据业务场景选择,新手先从粗粒度锁开始。
3. 互斥锁 vs 自旋锁
| 类型 | 特点 | 适用场景 |
|---|---|---|
| 互斥锁 | 加锁失败则阻塞(让出 CPU) | 锁持有时间长(如 IO 操作) |
| 自旋锁 | 加锁失败则循环重试(占用 CPU) | 锁持有时间极短(如简单计数) |
四、互斥锁属性(进阶)
通过 pthread_mutexattr_t 可自定义锁属性,常见属性:
- 普通锁(默认):重复加锁死锁,解锁非持有线程未定义;
- 错误检查锁 :重复加锁返回
EDEADLK,解锁非持有线程返回EPERM; - 递归锁:允许同一线程多次加锁(需对应次数解锁);
示例:初始化递归锁
pthread_mutex_t mutex;
pthread_mutexattr_t attr;
// 初始化属性
pthread_mutexattr_init(&attr);
// 设置为递归锁
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 用自定义属性初始化锁
pthread_mutex_init(&mutex, &attr);
// 递归加锁(允许)
pthread_mutex_lock(&mutex);
pthread_mutex_lock(&mutex);
count += 10;
// 需解锁两次
pthread_mutex_unlock(&mutex);
pthread_mutex_unlock(&mutex);
// 销毁属性和锁
pthread_mutexattr_destroy(&attr);
pthread_mutex_destroy(&mutex);
五、总结
- 互斥锁核心作用:保证同一时间只有一个线程操作共享资源,解决竞态条件;
- 核心函数 :
- 初始化:
pthread_mutex_init/PTHREAD_MUTEX_INITIALIZER; - 加锁:
pthread_mutex_lock(阻塞)/pthread_mutex_trylock(非阻塞); - 解锁:
pthread_mutex_unlock; - 销毁:
pthread_mutex_destroy;
- 初始化:
- 关键注意点 :
- 避免死锁(重复加锁、交叉锁);
- 解锁前必须加锁,且只有持有锁的线程能解锁;
- 根据锁持有时间选择互斥锁(长)或自旋锁(短)。
- 进程间的互斥锁
-
需要注意的是互斥锁(Mutex)主要用于多线程环境中的同步,但在多进程环境中使用互斥锁需要特别注意。标准的 POSIX 互斥锁(
pthread_mutex_t)是线程私有的,不能直接用于多进程之间的同步。这是因为每个进程都有独立的地址空间,互斥锁的状态信息(如锁的持有者、锁的状态等)存储在进程的内存中,无法在不同的进程之间共享。 -
如果需要在多个进程之间共享互斥锁,必须使用
PTHREAD_PROCESS_SHARED属性,并且互斥锁需要存储在共享内存中,这种方式过于冗杂。实际使用时,还是建议使用信号量进行资源多进程资源管理。