378 lines
5.0 KiB
Markdown
378 lines
5.0 KiB
Markdown
很好,这一步其实就是把你刚刚理解的“信号量体系”再精细化一下。
|
||
如果说信号量是“通用同步工具”,那互斥量(Mutex)就是一个**专门为“互斥”优化的特化版本**。
|
||
|
||
我按你现在的理解层级来讲:从本质 → 和信号量的关系 → 实现 → 设计差异。
|
||
|
||
---
|
||
|
||
# 一、互斥量是什么(本质)
|
||
|
||
互斥量(Mutex, mutual exclusion)本质上是:
|
||
|
||
> **一个带“所有权”的二值锁**
|
||
|
||
状态只有两种:
|
||
|
||
```text
|
||
locked (已上锁)
|
||
unlocked (未上锁)
|
||
```
|
||
|
||
但关键点在这里:
|
||
|
||
> **谁加锁,必须由谁解锁**
|
||
|
||
这点和信号量是本质区别。
|
||
|
||
---
|
||
|
||
# 二、最直观理解
|
||
|
||
你可以这样对比:
|
||
|
||
## 信号量(Semaphore)
|
||
|
||
```text
|
||
像“公共许可证”
|
||
谁拿都行,谁还也行
|
||
```
|
||
|
||
---
|
||
|
||
## 互斥量(Mutex)
|
||
|
||
```text
|
||
像“私人钥匙”
|
||
谁锁门,必须谁开门
|
||
```
|
||
|
||
---
|
||
|
||
# 三、基本操作
|
||
|
||
互斥量通常只有两个操作:
|
||
|
||
```c
|
||
lock(mutex) // 加锁
|
||
unlock(mutex) // 解锁
|
||
```
|
||
|
||
语义:
|
||
|
||
---
|
||
|
||
### lock
|
||
|
||
```c
|
||
if (mutex 已被占用)
|
||
阻塞
|
||
否则
|
||
获取锁(标记为已占用)
|
||
记录当前线程为 owner
|
||
```
|
||
|
||
---
|
||
|
||
### unlock
|
||
|
||
```c
|
||
如果 当前线程 != owner
|
||
报错(或未定义行为)
|
||
|
||
释放锁
|
||
唤醒一个等待线程
|
||
```
|
||
|
||
---
|
||
|
||
# 四、和信号量的关系(非常重要)
|
||
|
||
你可以认为:
|
||
|
||
> **Mutex ≈ Binary Semaphore + Ownership**
|
||
|
||
但这个“≈”是有坑的。
|
||
|
||
---
|
||
|
||
## 相同点
|
||
|
||
* 都可以实现互斥
|
||
* 都会阻塞线程
|
||
* 底层都依赖原子操作
|
||
|
||
---
|
||
|
||
## 不同点(核心)
|
||
|
||
| 特性 | Semaphore | Mutex |
|
||
| ---- | --------- | ------ |
|
||
| 计数 | 可以 >1 | 只能 0/1 |
|
||
| 所有权 | ❌ 没有 | ✅ 有 |
|
||
| 误用检测 | ❌ 没有 | ✅ 可以检测 |
|
||
| 用途 | 同步 + 互斥 | 纯互斥 |
|
||
|
||
---
|
||
|
||
### 一个关键例子
|
||
|
||
信号量允许:
|
||
|
||
```c
|
||
// 线程A
|
||
P(S)
|
||
|
||
// 线程B
|
||
V(S)
|
||
```
|
||
|
||
合法 ✅
|
||
|
||
---
|
||
|
||
但 mutex 不允许:
|
||
|
||
```c
|
||
线程A: lock(m)
|
||
线程B: unlock(m) // ❌ 错误
|
||
```
|
||
|
||
---
|
||
|
||
# 五、为什么需要 Mutex(而不是只用信号量)
|
||
|
||
这是设计哲学问题,很关键。
|
||
|
||
---
|
||
|
||
## 1. 防止“逻辑错误”
|
||
|
||
信号量太自由:
|
||
|
||
```c
|
||
P(S)
|
||
// 忘了 V(S)
|
||
```
|
||
|
||
或者:
|
||
|
||
```c
|
||
多 V 了一次
|
||
```
|
||
|
||
系统不会报错,但逻辑已经炸了。
|
||
|
||
---
|
||
|
||
Mutex:
|
||
|
||
```c
|
||
unlock 非 owner
|
||
```
|
||
|
||
→ 可以直接检测错误
|
||
|
||
---
|
||
|
||
## 2. 表达语义更清晰
|
||
|
||
```c
|
||
mutex.lock()
|
||
// 临界区
|
||
mutex.unlock()
|
||
```
|
||
|
||
比:
|
||
|
||
```c
|
||
P(S)
|
||
// ...
|
||
V(S)
|
||
```
|
||
|
||
更直观:
|
||
|
||
> “我就是要保护这段代码”
|
||
|
||
---
|
||
|
||
## 3. 性能优化空间更大
|
||
|
||
Mutex 可以做很多优化(后面讲)
|
||
|
||
---
|
||
|
||
# 六、实现原理(比信号量更复杂一点)
|
||
|
||
你现在重点要理解两个阶段:
|
||
|
||
---
|
||
|
||
## 阶段1:用户态快速路径(fast path)
|
||
|
||
现代 mutex(比如 Linux pthread_mutex):
|
||
|
||
```c
|
||
if (CAS(&lock, 0, 1))
|
||
成功获取锁(不进内核)
|
||
```
|
||
|
||
---
|
||
|
||
## 阶段2:失败 → 进入内核(slow path)
|
||
|
||
```c
|
||
获取失败:
|
||
进入内核
|
||
加入等待队列
|
||
阻塞
|
||
```
|
||
|
||
---
|
||
|
||
# 七、关键优化:futex(你这个阶段非常值得了解)
|
||
|
||
在 Linux 中:
|
||
|
||
> **mutex ≈ futex + 用户态CAS**
|
||
|
||
futex = fast userspace mutex
|
||
|
||
核心思想:
|
||
|
||
```text
|
||
能不进内核就不进内核
|
||
```
|
||
|
||
---
|
||
|
||
流程:
|
||
|
||
```text
|
||
加锁:
|
||
CAS 成功 → 完成(用户态)
|
||
CAS 失败 → futex_wait(进入内核)
|
||
|
||
解锁:
|
||
CAS 释放
|
||
如果有人等待 → futex_wake
|
||
```
|
||
|
||
---
|
||
|
||
# 八、Mutex 的几种类型(高级但重要)
|
||
|
||
---
|
||
|
||
## 1. 普通 mutex
|
||
|
||
最基础的锁
|
||
|
||
---
|
||
|
||
## 2. 可重入锁(recursive mutex)
|
||
|
||
允许:
|
||
|
||
```c
|
||
lock()
|
||
lock()
|
||
unlock()
|
||
unlock()
|
||
```
|
||
|
||
同一线程多次加锁
|
||
|
||
---
|
||
|
||
## 3. 自旋锁(spinlock)
|
||
|
||
```text
|
||
不阻塞,一直忙等
|
||
```
|
||
|
||
适合:
|
||
|
||
* 临界区极短
|
||
* 内核中断上下文
|
||
|
||
---
|
||
|
||
## 4. 读写锁(rwlock)
|
||
|
||
```text
|
||
多个读可以并发
|
||
写必须独占
|
||
```
|
||
|
||
---
|
||
|
||
# 九、经典问题:优先级反转
|
||
|
||
这个是 mutex 才有的典型问题:
|
||
|
||
```text
|
||
低优先级线程持有锁
|
||
高优先级线程在等
|
||
中优先级线程一直抢CPU
|
||
```
|
||
|
||
→ 高优先级线程反而跑不了
|
||
|
||
---
|
||
|
||
解决:
|
||
|
||
> **优先级继承(priority inheritance)**
|
||
|
||
---
|
||
|
||
# 十、总结(给你一个结构化认知)
|
||
|
||
你现在可以这样构建体系:
|
||
|
||
---
|
||
|
||
## 同步工具分层
|
||
|
||
```text
|
||
硬件层:
|
||
原子指令(CAS)
|
||
|
||
内核层:
|
||
futex / 调度器
|
||
|
||
抽象层:
|
||
semaphore(通用)
|
||
mutex(互斥专用)
|
||
```
|
||
|
||
---
|
||
|
||
## 核心区别一句话
|
||
|
||
> **Semaphore 是“资源计数器”,Mutex 是“带所有权的锁”**
|
||
|
||
---
|
||
|
||
# 十一、一个建议(非常实用)
|
||
|
||
你以后写代码:
|
||
|
||
* 需要“限制资源数量” → 用 semaphore
|
||
* 需要“保护临界区” → 用 mutex
|
||
|
||
---
|
||
|
||
如果你想继续深入,我可以带你把这几个东西彻底打通(强烈建议):
|
||
|
||
* pthread_mutex 的源码级实现
|
||
* futex 的 syscall 细节
|
||
* 自旋锁 vs 阻塞锁的性能边界
|
||
* C++ `std::mutex` 和 `std::atomic` 的关系
|
||
|
||
这些一旦理解,你对“并发”这一块基本就是工程级水平了。
|
||
|
||
---
|
||
|