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