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()

示意图如下:

CoutingSemaphore

3.3 二值信号量(Binary Semaphore)

只能取 0 或 1,功能上等同于 Mutex,但实现机制不同。

示意图如下:

BinarySemaphore


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 包),避免“只会用不会调”。



原始标题:Semaphore vs. Mutex