DAY 12-线程同步与死锁

3 阅读6分钟

1. 死锁:四条件与破解之道

问:死锁是什么?发生死锁需要满足哪四个条件?

死锁是指两个或多个线程(进程)互相等待对方持有的资源,导致所有人都无法继续执行。

发生死锁必须同时满足以下四个条件:

条件含义
互斥资源一次只能被一个线程占用
请求与保持线程占有一个资源,同时请求另一个资源
不可剥夺已分配的资源不能被强行抢走
循环等待线程间形成环形等待链

典型场景

线程 A                线程 B
   │                      │
   ▼                      ▼
 锁住 X                 锁住 Y
   │                      │
   ▼                      ▼
 请求 Y ←── 死锁 ──→ 请求 X
   │                      │
   ▼                      ▼
  等待                   等待

问:怎么避免死锁?

策略做法
预防破坏四个条件之一。工程中最常用固定加锁顺序(破坏循环等待)——所有线程按同一顺序加锁
避免银行家算法:分配前先试算,判断是否会导致死锁
检测与恢复检测到循环等待后,终止进程或回滚

工程最佳实践

// ❌ 危险:不同顺序加锁
void thread_a() {
    pthread_mutex_lock(&lock1);
    pthread_mutex_lock(&lock2);  // 先 1 后 2
}

void thread_b() {
    pthread_mutex_lock(&lock2);
    pthread_mutex_lock(&lock1);  // 先 2 后 1 → 可能死锁
}

// ✅ 安全:统一加锁顺序
void thread_a() {
    pthread_mutex_lock(&lock1);
    pthread_mutex_lock(&lock2);  // 先 1 后 2
}

void thread_b() {
    pthread_mutex_lock(&lock1);  // 也先 1
    pthread_mutex_lock(&lock2);  // 后 2
}

如果无法固定顺序,可以用 pthread_mutex_trylock + 超时退避:

while (1) {
    pthread_mutex_lock(&lock1);
    if (pthread_mutex_trylock(&lock2) == 0) {
        break;  // 拿到两个锁
    }
    pthread_mutex_unlock(&lock1);
    usleep(100);  // 退避后重试
}

2. 条件变量与虚假唤醒

问:为什么 pthread_cond_wait 必须放在 while 循环里而不是 if

因为存在虚假唤醒(spurious wakeup)——即使没有其他线程调用 pthread_cond_signalpthread_cond_wait 也可能返回。

原因:

  • 某些实现的内部机制
  • 被系统信号中断
  • 多核处理器下的竞争条件
// ❌ 错误:用 if
pthread_mutex_lock(&mtx);
if (queue_is_empty()) {          // 只检查一次
    pthread_cond_wait(&cv, &mtx);
}
// 虚假唤醒时队列可能还是空的,但这里直接取数据 → 出错
data = queue_pop();
pthread_mutex_unlock(&mtx);

// ✅ 正确:用 while
pthread_mutex_lock(&mtx);
while (queue_is_empty()) {       // 每次唤醒都重新检查
    pthread_cond_wait(&cv, &mtx);
}
data = queue_pop();              // 条件满足才执行
pthread_mutex_unlock(&mtx);

核心理解while 确保条件满足才继续,不满足就继续等。这是条件变量的标准编程范式

生产者-消费者完整示例

// 生产者
void* producer(void* arg) {
    while (1) {
        pthread_mutex_lock(&mtx);
        while (queue_is_full()) {    // 满了就等
            pthread_cond_wait(&full_cv, &mtx);
        }
        queue_push(data);
        pthread_cond_signal(&empty_cv);  // 通知消费者
        pthread_mutex_unlock(&mtx);
    }
}

// 消费者
void* consumer(void* arg) {
    while (1) {
        pthread_mutex_lock(&mtx);
        while (queue_is_empty()) {   // 空了就等
            pthread_cond_wait(&empty_cv, &mtx);
        }
        data = queue_pop();
        pthread_cond_signal(&full_cv);   // 通知生产者
        pthread_mutex_unlock(&mtx);
    }
}

3. 优先级反转

问:什么是优先级反转?怎么解决?

现象:高优先级任务等待低优先级任务持有的资源,但低优先级任务被中等优先级任务抢占,导致高优先级反而等中等优先级。

高优先级任务 ──等待──┐
                      ▼
低优先级任务 持有资源  ← 高优先级等低优先级
      │
      ▼
中等优先级任务  抢占低优先级(不相关任务,但系统优先跑它)
      │
      ▼
高优先级被阻塞,等低优先级跑完才能继续

解决方案

方案做法
优先级继承低优先级任务持有资源时,临时提升到等待它的最高优先级
优先级天花板每个资源预设一个优先级上限,获取资源时自动提升

优先级继承示例

正常:高(100) > 中(50) > 低(20)
低持有锁 → 高等低 → 低临时提升到 100 → 低不会被中抢占 → 低释放锁 → 恢复优先级

4. 三种同步原语对比

问:互斥锁、条件变量、读写锁、自旋锁分别用在什么场景?

原语场景特点
互斥锁 (mutex)互斥访问临界区谁加锁谁解锁,可能睡眠
条件变量 (cond)生产者-消费者模型必须配合互斥锁使用
读写锁 (rwlock)读多写少多读单写,读共享、写独占
自旋锁 (spinlock)临界区极短忙等待,不睡眠,仅用于原子上下文

选择原则

场景推荐
临界区代码长,可能睡眠互斥锁
需要等待某个条件满足条件变量
读操作远多于写操作读写锁
临界区极短(几条指令),或中断上下文自旋锁

5. 中断与锁

问:中断里想和用户进程做同步,该用哪种锁?

必须用自旋锁

原因:

锁类型在中断中能否用原因
互斥锁❌ 不能可能睡眠/调度,中断上下文没有进程上下文
读写锁❌ 不能可能睡眠
信号量❌ 不能(down 可能睡眠)可能睡眠
自旋锁✅ 能用忙等待,不睡眠

中断中加锁的正确方式

// ❌ 错误:中断中用互斥锁
void interrupt_handler() {
    pthread_mutex_lock(&mtx);  // 可能睡眠 → 崩溃
    // ...
    pthread_mutex_unlock(&mtx);
}

// ✅ 正确:中断中用自旋锁 + 关中断
void interrupt_handler() {
    unsigned long flags;
    spin_lock_irqsave(&lock, flags);  // 关中断 + 加锁
    // 临界区(极短)
    spin_unlock_irqrestore(&lock, flags);
}

spin_lock_irqsave 做了两件事

  1. 保存当前中断状态
  2. 关闭本地中断 + 加自旋锁

防止在持有自旋锁期间被同一个中断再次打断导致死锁。


6. 总结

线程同步知识图谱
│
├── 死锁(四条件)
│   ├── 互斥、请求与保持、不可剥夺、循环等待
│   └── 破解:固定加锁顺序、trylock+退避
│
├── 条件变量
│   ├── 必须配合 mutex 使用
│   └── while + wait 标准范式(防虚假唤醒)
│
├── 优先级反转
│   ├── 高优先级等低优先级,被中优先级抢占
│   └── 解决:优先级继承 / 优先级天花板
│
└── 锁的选择
    ├── 互斥锁 → 临界区长、会睡眠
    ├── 条件变量 → 生产者-消费者
    ├── 读写锁 → 读多写少
    └── 自旋锁 → 临界区极短、中断上下文

核心理解

  • 死锁的本质是资源竞争,统一加锁顺序是工程中最简单有效的预防手段。
  • 条件变量的正确用法是 while + pthread_cond_wait,虚假唤醒要求你每次唤醒后重新检查条件。
  • 中断上下文只能用自旋锁,因为其他锁可能睡眠,而中断不能睡眠。spin_lock_irqsave 是标准写法。