244 lines
4.1 KiB
Markdown
244 lines
4.1 KiB
Markdown
线程互斥本质上是在解决一个问题:**多个线程同时访问共享资源时,如何保证“同一时刻只有一个线程在操作”**。不同方案的差别主要在于:是否进入内核、是否忙等、性能如何、适用场景。
|
||
|
||
我按“从底层到高级”的逻辑给你梳一遍,这样你能建立一条完整的认知链。
|
||
|
||
---
|
||
|
||
## 1. 关闭中断(最底层,仅限内核)
|
||
|
||
**思路:**
|
||
在临界区执行前关闭中断,执行完再打开。
|
||
|
||
**效果:**
|
||
|
||
* 当前 CPU 不会被打断
|
||
* 不会发生线程切换 → 自然不会并发
|
||
|
||
**问题:**
|
||
|
||
* 只对单核有效(多核直接失效)
|
||
* 用户态做不了
|
||
* 会影响系统响应(很危险)
|
||
|
||
**结论:**
|
||
这是操作系统内核里的“原始手段”,你可以理解为最暴力的锁。
|
||
|
||
---
|
||
|
||
## 2. 原子操作(Atomic / CAS)
|
||
|
||
**核心:**
|
||
CPU 提供的不可分割操作,比如:
|
||
|
||
* Test-and-Set
|
||
* Compare-and-Swap(CAS)
|
||
|
||
**典型代码逻辑:**
|
||
|
||
```c
|
||
while (CAS(lock, 0, 1) == false) {
|
||
// busy wait
|
||
}
|
||
```
|
||
|
||
**特点:**
|
||
|
||
* 不进入内核(用户态)
|
||
* 极快
|
||
* 但会**忙等(spin)**
|
||
|
||
---
|
||
|
||
## 3. 自旋锁(Spinlock)
|
||
|
||
其实就是“原子操作 + 死循环”:
|
||
|
||
```c
|
||
while (lock == 1); // 一直等
|
||
lock = 1;
|
||
```
|
||
|
||
实际实现用 CAS 保证安全。
|
||
|
||
**特点:**
|
||
|
||
* 不睡眠,一直占 CPU
|
||
* 适合:
|
||
|
||
* 临界区很短
|
||
* 多核环境
|
||
|
||
**问题:**
|
||
|
||
* 临界区长 → CPU白烧
|
||
* 单核上很蠢(等自己)
|
||
|
||
---
|
||
|
||
## 4. 互斥锁(Mutex)
|
||
|
||
这是你最常用的。
|
||
|
||
**核心思想:**
|
||
拿不到锁 → 线程**睡眠(阻塞)**
|
||
|
||
**流程:**
|
||
|
||
1. 尝试获取锁(CAS)
|
||
2. 失败 → 进入内核
|
||
3. 被挂起(加入等待队列)
|
||
4. 被唤醒再竞争
|
||
|
||
**特点:**
|
||
|
||
* 不浪费 CPU
|
||
* 需要系统调用(慢一点)
|
||
|
||
**一句话理解:**
|
||
|
||
> 自旋锁是“在门口死等”,Mutex 是“拿号排队等叫号”。
|
||
|
||
---
|
||
|
||
## 5. 读写锁(Read-Write Lock)
|
||
|
||
**优化场景:读多写少**
|
||
|
||
规则:
|
||
|
||
* 多个读可以同时进行
|
||
* 写必须独占
|
||
|
||
**效果:**
|
||
|
||
* 提升并发性能
|
||
|
||
**问题:**
|
||
|
||
* 写可能被饿死(读太多)
|
||
|
||
---
|
||
|
||
## 6. 信号量(Semaphore)
|
||
|
||
本质是一个**计数器**。
|
||
|
||
### 两种:
|
||
|
||
* 二值信号量(≈ Mutex)
|
||
* 计数信号量(允许 N 个线程进入)
|
||
|
||
**经典操作:**
|
||
|
||
* P(wait):减1,不够就阻塞
|
||
* V(signal):加1,唤醒别人
|
||
|
||
**适合:**
|
||
|
||
* 资源池(比如连接池)
|
||
|
||
---
|
||
|
||
## 7. 条件变量(Condition Variable)
|
||
|
||
不是锁,是**配合 Mutex 用的**
|
||
|
||
**用途:**
|
||
线程等待某个条件成立
|
||
|
||
```c
|
||
pthread_cond_wait(cond, mutex);
|
||
```
|
||
|
||
**特点:**
|
||
|
||
* 自动释放 mutex
|
||
* 被唤醒后重新加锁
|
||
|
||
**典型场景:**
|
||
生产者-消费者
|
||
|
||
---
|
||
|
||
## 8. 乐观锁(无锁编程)
|
||
|
||
**核心思想:**
|
||
|
||
> 不加锁,赌冲突很少
|
||
|
||
用 CAS + 重试:
|
||
|
||
```c
|
||
do {
|
||
old = x;
|
||
new = old + 1;
|
||
} while (!CAS(&x, old, new));
|
||
```
|
||
|
||
**特点:**
|
||
|
||
* 无锁(lock-free)
|
||
* 高性能
|
||
* 代码复杂,容易错
|
||
|
||
---
|
||
|
||
## 9. Futex(Linux关键机制)
|
||
|
||
你前面学内核,这个必须知道。
|
||
|
||
**全称:**
|
||
Fast Userspace Mutex
|
||
|
||
**核心思想:**
|
||
|
||
* 优先在用户态自旋
|
||
* 实在不行再进入内核睡眠
|
||
|
||
**流程:**
|
||
|
||
1. CAS尝试拿锁
|
||
2. 失败 → futex_wait(进内核)
|
||
3. 解锁 → futex_wake
|
||
|
||
**一句话:**
|
||
|
||
> Mutex = 自旋锁 + 睡眠(futex实现)
|
||
|
||
---
|
||
|
||
## 总结一张图(抽象层级)
|
||
|
||
从低到高:
|
||
|
||
```
|
||
关闭中断(内核)
|
||
↓
|
||
原子操作(CAS)
|
||
↓
|
||
自旋锁(忙等)
|
||
↓
|
||
Mutex(阻塞)
|
||
↓
|
||
读写锁 / 信号量 / 条件变量(高级同步)
|
||
↓
|
||
无锁编程(CAS体系)
|
||
```
|
||
|
||
---
|
||
|
||
## 给你一个“工程选型直觉”
|
||
|
||
你以后写程序可以这样选:
|
||
|
||
* 临界区极短 → 自旋锁
|
||
* 普通情况 → Mutex
|
||
* 读多写少 → 读写锁
|
||
* 资源数量控制 → 信号量
|
||
* 线程协作 → 条件变量
|
||
* 极致性能 → CAS / 无锁
|
||
|
||
---
|
||
|
||
如果你想更进一步,我可以帮你把这些**全部映射到 Linux 内核实现(futex、task_struct、调度器)**,甚至给你画出“线程阻塞时内核到底干了什么”的完整路径,那一块才是真正把知识打通的地方。
|