Skip to content

同步、互斥与死锁

同步与互斥

  • 互斥(mutual exclusion):同一时刻最多一个执行单元进入临界区访问某共享资源,避免竞态条件。
  • 同步(synchronization):协调多个执行单元的先后顺序,使某个操作在条件满足后才进行,例如“消费者等待生产者放入数据”。

临界区的基本结构:

text
进入区:获取锁 / 等待条件
临界区:访问共享资源
退出区:释放锁 / 通知等待者

正确的并发程序不只要互斥,还要保证进展性和有限等待,避免死锁、活锁与饥饿。

常用同步原语

互斥锁(mutex)

互斥锁只有“已锁/未锁”两种基本状态,持锁者退出临界区时释放。适合保护共享数据结构。必须用 RAII(如 C++ 的 std::lock_guardstd::unique_lock)保证异常或提前返回时释放锁。

自旋锁(spinlock)

获取不到锁时不睡眠而循环检查。它避免线程休眠/唤醒开销,适合临界区极短且不能睡眠的场景(如内核部分路径);竞争时间长会浪费 CPU,不宜用于普通长操作或 I/O。

读写锁(RWLock)

多个读者可并行持有读锁;写者独占。读远多于写时可提高吞吐,但需选择读优先、写优先或公平策略,避免一方饥饿。

信号量(semaphore)与 P/V 操作

信号量是非负计数与等待队列的组合:

  • P / wait / down:原子地尝试将计数减一;若无可用资源则阻塞;
  • V / signal / up:原子地将计数加一,并在需要时唤醒等待者。

计数为 1 的二元信号量可实现互斥;计数为 N 的信号量可表示 N 个同类资源或控制并发量。

条件变量(condition variable)

条件变量不存储“条件本身”,它让线程在某个谓词不满足时原子地“释放互斥锁并睡眠”。被唤醒后必须重新获得锁并在循环中重新检查条件,以应对虚假唤醒和竞争:

cpp
std::unique_lock lock(mutex);
cv.wait(lock, [&] { return !queue.empty(); });
auto item = queue.front();

死锁

死锁是多个执行单元相互等待对方持有资源,且都不能继续推进的状态。经典 Coffman 必要条件同时成立才可能产生死锁:

  1. 互斥:资源不能同时被多个执行单元使用;
  2. 请求并保持:持有资源的同时继续申请新资源;
  3. 不可剥夺:资源不能被强制收回;
  4. 循环等待:形成等待资源的环。

应对方式

  • 预防:破坏必要条件,例如按全局固定顺序申请多个锁、一次性申请资源、允许可回收资源。
  • 避免:在分配时判断系统是否仍处于安全状态,例如银行家算法(理论/特定场景)。
  • 检测与恢复:建立等待图或资源分配图检测环;发生后重试、回滚、抢占资源或终止部分任务。
  • 工程实践:缩短临界区;不在持锁时做阻塞 I/O;统一锁顺序;使用 std::scoped_lock 一次锁多个 mutex;设置超时/取消和可观测性。

常见并发问题

  • 竞态条件:结果依赖不可控的执行交错;用正确同步、原子操作或无锁设计解决。
  • 活锁:任务都在响应对方而持续重试,状态不断变化但没有进展;常用随机退避等方式缓解。
  • 饥饿:某任务长期得不到资源;需公平策略、优先级老化或限流。

使用 Markdown 与 VitePress 构建