13. 死锁与同步

深入理解死锁的四个必要条件、预防与避免策略,掌握信号量、互斥锁、管程、条件变量等经典同步机制。

1. 死锁的定义

死锁:多个进程因竞争资源而相互等待,若无外力干预,永远都无法继续执行。

进程 P1 持有资源 A,等待资源 B
进程 P2 持有资源 B,等待资源 A

        P1 ───持有──→ 资源 A
        ↑              │
        │              ↓
       等待            等待
        │              ↓
        P2 ←──持有─── 资源 B

2. 死锁的四个必要条件

条件说明破坏策略
互斥资源一次只能被一个进程占用共享资源(如只读文件)
占有并等待持有资源同时等待新资源一次性申请所有资源
不可抢占已分配的资源不能被强制剥夺资源可被抢占
循环等待进程-资源形成环资源有序分配

四个条件必须同时满足才会死锁,破坏任一条件即可预防。


3. 死锁处理策略

3.1 死锁预防

策略 1:破坏"占有并等待"
  → 进程运行前一次性申请所有所需资源
  → 缺点:资源利用率低,可能长时间占用不急需的资源

策略 2:破坏"不可抢占"
  → 进程申请新资源失败时,释放已持有的所有资源
  → 适用于状态可保存/恢复的资源

策略 3:破坏"循环等待"
  → 给资源编号,进程必须按编号递增顺序申请
  → 例:先申请打印机(1),再申请磁盘(2)

3.2 死锁避免(银行家算法)

在资源分配前检查系统是否处于安全状态,若可能导致不安全则不分配。

def bank_algorithm(available, max_claim, allocation):
    """
    available: 可用资源向量
    max_claim: 各进程最大需求矩阵
    allocation: 当前分配矩阵
    need = max_claim - allocation
    """
    n = len(max_claim)      # 进程数
    m = len(available)      # 资源种类数
    need = [[max_claim[i][j] - allocation[i][j]
             for j in range(m)] for i in range(n)]

    work = available[:]
    finish = [False] * n
    safe_sequence = []

    while len(safe_sequence) < n:
        found = False
        for i in range(n):
            if finish[i]:
                continue
            # 检查 need[i] <= work
            if all(need[i][j] <= work[j] for j in range(m)):
                for j in range(m):
                    work[j] += allocation[i][j]
                finish[i] = True
                safe_sequence.append(i)
                found = True
                break
        if not found:
            return None  # 不安全状态

    return safe_sequence

# 示例
available = [3, 3, 2]
max_claim = [[7, 5, 3], [3, 2, 2], [9, 0, 2], [2, 2, 2], [4, 3, 3]]
allocation = [[0, 1, 0], [2, 0, 0], [3, 0, 2], [2, 1, 1], [0, 0, 2]]

print(bank_algorithm(available, max_claim, allocation))
# 输出: [1, 3, 4, 0, 2](安全序列)

3.3 死锁检测与恢复

检测:定期运行死锁检测算法(资源分配图是否有环)
恢复:
  1. 进程终止:逐个终止死锁进程直到环解除
  2. 资源剥夺:从某进程抢占资源分配给其他进程

4. 同步机制

4.1 信号量(Semaphore)

#include <semaphore.h>

// 初始化
sem_t sem;
sem_init(&sem, 0, 1);  // 第二个 0=线程间,1=初始值

// P 操作(等待/减 1)
sem_wait(&sem);        // 值 > 0 则减 1 继续;= 0 则阻塞

// V 操作(信号/加 1)
sem_post(&sem);        // 加 1,唤醒等待线程

// 销毁
sem_destroy(&sem);

用信号量实现互斥:

sem_t mutex;  // 初始值 = 1

void critical_section() {
    sem_wait(&mutex);      // P,进入临界区
    // 临界区代码
    sem_post(&mutex);      // V,离开临界区
}

用信号量实现生产者-消费者:

sem_t empty;  // 空槽数量,初始 = N
sem_t full;   // 满槽数量,初始 = 0
sem_t mutex;  // 互斥访问缓冲区,初始 = 1

void producer() {
    while (1) {
        item = produce();
        sem_wait(&empty);   // 等待空槽
        sem_wait(&mutex);   // 进入临界区
        buffer[in] = item;
        in = (in + 1) % N;
        sem_post(&mutex);   // 离开临界区
        sem_post(&full);    // 满槽 + 1
    }
}

void consumer() {
    while (1) {
        sem_wait(&full);    // 等待满槽
        sem_wait(&mutex);   // 进入临界区
        item = buffer[out];
        out = (out + 1) % N;
        sem_post(&mutex);   // 离开临界区
        sem_post(&empty);   // 空槽 + 1
        consume(item);
    }
}

注意sem_wait(&empty)sem_wait(&mutex) 顺序不能交换,否则可能死锁。

4.2 互斥锁(Mutex)

#include <pthread.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_lock(&mutex);     // 加锁
// 临界区
pthread_mutex_unlock(&mutex);   // 解锁

互斥锁 vs 信号量

特性互斥锁信号量
初始值1(解锁)或 0(锁定)任意非负整数
用途互斥互斥 + 同步
持有者有(只有加锁者能解锁)
可重入不可(同一线程多次加锁会死锁)可以

4.3 读写锁(Reader-Writer Lock)

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

// 读锁(多个读者可同时持有)
pthread_rwlock_rdlock(&rwlock);
// 读操作
pthread_rwlock_unlock(&rwlock);

// 写锁(独占)
pthread_rwlock_wrlock(&rwlock);
// 写操作
pthread_rwlock_unlock(&rwlock);

适用:读多写少场景,如缓存系统。

4.4 条件变量(Condition Variable)

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int ready = 0;

// 等待方
void waiter() {
    pthread_mutex_lock(&mutex);
    while (!ready) {              // 必须用 while(防止虚假唤醒)
        pthread_cond_wait(&cond, &mutex);  // 原子释放锁并阻塞
    }
    // 条件满足,继续执行
    pthread_mutex_unlock(&mutex);
}

// 通知方
void notifier() {
    pthread_mutex_lock(&mutex);
    ready = 1;
    pthread_cond_signal(&cond);   // 唤醒一个等待线程
    // pthread_cond_broadcast(&cond);  // 唤醒所有等待线程
    pthread_mutex_unlock(&mutex);
}

为什么用 while 不用 if:被唤醒后应重新检查条件,因为可能其他线程已修改条件(虚假唤醒)。

4.5 管程(Monitor)

管程 = 共享数据 + 操作这组数据的过程 + 互斥锁 + 条件变量,封装在一起。

// Java synchronized 即管程支持
public class BoundedBuffer<T> {
    private final T[] buffer;
    private int count = 0, putptr = 0, takeptr = 0;

    public synchronized void put(T x) throws InterruptedException {
        while (count == buffer.length) {
            wait();  // 等待未满条件
        }
        buffer[putptr] = x;
        putptr = (putptr + 1) % buffer.length;
        count++;
        notifyAll();  // 通知可能等待的 take()
    }

    public synchronized T take() throws InterruptedException {
        while (count == 0) {
            wait();  // 等待非空条件
        }
        T x = buffer[takeptr];
        takeptr = (takeptr + 1) % buffer.length;
        count--;
        notifyAll();  // 通知可能等待的 put()
        return x;
    }
}

5. 经典同步问题

5.1 哲学家就餐问题

五位哲学家围桌而坐,每人需要左右两把叉子才能就餐。如何避免死锁?

解决方案

  1. 最多允许 4 位哲学家同时拿叉(信号量限制)
  2. 左右编号不一致时按顺序拿(破坏循环等待)
  3. 使用互斥锁保护拿叉动作(原子化)
// 方案 3:线程互斥拿叉
pthread_mutex_t mutex;

void philosopher(int i) {
    pthread_mutex_lock(&mutex);      // 原子拿两把叉子
    take_fork(i);                    // 左叉
    take_fork((i + 1) % 5);          // 右叉
    pthread_mutex_unlock(&mutex);    // 释放锁(叉仍拿着)
    eat();
    put_fork(i);
    put_fork((i + 1) % 5);
}

5.2 读者-写者问题

允许多个读者同时读,但写者独占。

int reader_count = 0;
pthread_mutex_t rc_mutex = PTHREAD_MUTEX_INITIALIZER;  // 保护 reader_count
pthread_mutex_t rw_mutex = PTHREAD_MUTEX_INITIALIZER;  // 写者互斥 / 第一个读者/最后一个读者

void reader() {
    pthread_mutex_lock(&rc_mutex);
    reader_count++;
    if (reader_count == 1) {
        pthread_mutex_lock(&rw_mutex);  // 第一个读者阻止写者
    }
    pthread_mutex_unlock(&rc_mutex);
    // 读...
    pthread_mutex_lock(&rc_mutex);
    reader_count--;
    if (reader_count == 0) {
        pthread_mutex_unlock(&rw_mutex);  // 最后一个读者释放
    }
    pthread_mutex_unlock(&rc_mutex);
}

void writer() {
    pthread_mutex_lock(&rw_mutex);
    // 写...
    pthread_mutex_unlock(&rw_mutex);
}

6. 总结

死锁:
  预防:破坏四个必要条件之一
  避免:银行家算法(安全状态检查)
  检测:资源分配图环路检测
  恢复:终止进程 / 资源剥夺

同步原语选择:
  简单互斥       → 互斥锁
  计数限制       → 信号量
  读多写少       → 读写锁
  等待特定条件   → 条件变量 + 互斥锁
  高级封装       → 管程 / 语言级同步(Java synchronized)

参考文章

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「计算机基础」更多文章

  1. 16. 数据链路层
  2. 15. 网络层与路由
  3. 14. 网络模型与协议