1. 什么是临界区问题?
在操作系统中,多个进程(或线程)可能需要协作完成任务。它们之间通过共享内存或消息传递进行通信。当多个进程共享同一块地址空间时,必须控制它们的访问顺序,以防止数据竞争(race condition)和数据不一致等问题。
这种控制机制被称为同步(Synchronization)。如果没有合适的同步机制,进程可能会读取过期数据,或者覆盖其他进程的数据。
1.1 临界区的定义
每个进程都可能有一段代码,用于访问共享资源(如变量、文件、设备等),这段代码被称为临界区(Critical Section)。多个进程不能同时执行各自的临界区。
1.2 临界区问题的三个核心要求
为了解决临界区问题,一个良好的同步机制必须满足以下三个条件:
✅ 互斥(Mutual Exclusion):一个进程在执行临界区时,其他进程不能进入自己的临界区。
✅ 进展(Progress):如果没有任何进程在执行临界区,那么应能从等待的进程中选择一个进入临界区。
✅ 有限等待(Bounded Waiting):任何进程在请求进入临界区后,必须在有限时间内获得许可,不能无限期等待。
2. Mutex(互斥锁)
Mutex 是一种最基础的同步机制,用于保护临界区资源。它本质上是一个二值锁(locked/unlocked),只有持有锁的线程才能进入临界区。
2.1 Mutex 的工作原理
Mutex 提供两个基本操作:
acquire()
:获取锁。如果锁不可用,线程会被阻塞或忙等。release()
:释放锁,允许其他线程获取。
以下是伪代码示例:
algorithm acquire():
while not available:
// busy wait
available <- false
algorithm release():
available <- true
2.2 Mutex 的优缺点
✅ 优点:
- 实现简单。
- 避免了上下文切换(context switch)的开销,适用于短时间的锁定。
❌ 缺点:
- 会导致“忙等”(busy waiting),浪费 CPU 资源。
- 在单核 CPU 上表现不佳,因为忙等线程会占用 CPU 时间,无法让出资源。
3. Semaphore(信号量)
Semaphore 是一种更通用的同步机制,支持多个资源的访问控制。它通过两个原子操作 wait()
和 signal()
来管理资源的可用性。
3.1 Semaphore 的基本操作
wait(S)
:尝试获取资源。如果资源数大于 0,则减 1;否则忙等。signal(S)
:释放资源,资源数加 1。
伪代码如下:
algorithm wait(S):
while S <= 0:
// busy wait
S <- S - 1
algorithm signal(S):
S <- S + 1
Semaphore 可以是 计数信号量(Counting Semaphore) 或 二值信号量(Binary Semaphore)。
3.2 计数信号量(Counting Semaphore)
用于控制多个相同资源的并发访问。例如:
- 一个图书馆有 10 个自习室,每个自习室是一份资源。
- 每次有用户申请自习室时,调用
wait()
,释放时调用signal()
。
示意图如下:
3.3 二值信号量(Binary Semaphore)
只能取 0 或 1,功能上等同于 Mutex,但实现机制不同。
示意图如下:
4. Semaphore 与 Mutex 的对比
特性 | Semaphore | Mutex |
---|---|---|
类型 | 整数变量 | 对象 |
机制 | 信号机制(通知) | 锁机制(获取/释放) |
子类型 | 有计数和二值两种 | 无子类型 |
所有权 | 任意线程可以释放 | 只有加锁线程可以释放 |
操作 | 使用 wait() 和 signal() |
使用 lock() 和 unlock() |
资源管理 | 多线程可同时访问(取决于信号量值) | 仅允许一个线程访问 |
修改权限 | 只能通过 wait() 和 signal() 修改 |
由持有锁的线程修改 |
同时访问 | 支持多线程同时访问 | 不支持 |
⚠️ 踩坑提示:
不要将 Mutex 误认为是“轻量级的 Semaphore”,它们的设计初衷不同。Mutex 更强调“所有权”概念,适合保护单个资源;而 Semaphore 更适合资源池、生产者-消费者等多线程协调场景。
5. 总结
- Mutex 是最基础的同步工具,适合保护单一资源,强调“谁加锁谁释放”。
- Semaphore 更通用,支持多资源控制,适合复杂同步场景。
- 在实际开发中,根据场景选择合适的同步机制至关重要。
- Java 中的
ReentrantLock
是 Mutex 的实现,Semaphore
类则提供了完整的信号量支持。
✅ 建议:理解底层原理后,再使用高级封装库(如 Java 的 java.util.concurrent
包),避免“只会用不会调”。