Appearance
同步、互斥与死锁
同步与互斥
- 互斥(mutual exclusion):同一时刻最多一个执行单元进入临界区访问某共享资源,避免竞态条件。
- 同步(synchronization):协调多个执行单元的先后顺序,使某个操作在条件满足后才进行,例如“消费者等待生产者放入数据”。
临界区的基本结构:
text
进入区:获取锁 / 等待条件
临界区:访问共享资源
退出区:释放锁 / 通知等待者正确的并发程序不只要互斥,还要保证进展性和有限等待,避免死锁、活锁与饥饿。
常用同步原语
互斥锁(mutex)
互斥锁只有“已锁/未锁”两种基本状态,持锁者退出临界区时释放。适合保护共享数据结构。必须用 RAII(如 C++ 的 std::lock_guard、std::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 必要条件同时成立才可能产生死锁:
- 互斥:资源不能同时被多个执行单元使用;
- 请求并保持:持有资源的同时继续申请新资源;
- 不可剥夺:资源不能被强制收回;
- 循环等待:形成等待资源的环。
应对方式
- 预防:破坏必要条件,例如按全局固定顺序申请多个锁、一次性申请资源、允许可回收资源。
- 避免:在分配时判断系统是否仍处于安全状态,例如银行家算法(理论/特定场景)。
- 检测与恢复:建立等待图或资源分配图检测环;发生后重试、回滚、抢占资源或终止部分任务。
- 工程实践:缩短临界区;不在持锁时做阻塞 I/O;统一锁顺序;使用
std::scoped_lock一次锁多个 mutex;设置超时/取消和可观测性。
常见并发问题
- 竞态条件:结果依赖不可控的执行交错;用正确同步、原子操作或无锁设计解决。
- 活锁:任务都在响应对方而持续重试,状态不断变化但没有进展;常用随机退避等方式缓解。
- 饥饿:某任务长期得不到资源;需公平策略、优先级老化或限流。