首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >一文搞懂 | C语言编程中如何避免死锁以及死锁的四大必要条件

一文搞懂 | C语言编程中如何避免死锁以及死锁的四大必要条件

作者头像
C语言中文社区
发布2026-01-05 11:29:41
发布2026-01-05 11:29:41
610
举报
文章被收录于专栏:C语言中文社区C语言中文社区

正文

一、死锁的四个必要条件

死锁的发生是缺一不可的,必须同时满足以下4个条件,只要破坏其中任意一个条件,死锁就绝对不会发生,这是C语言(以及所有多线程/多进程编程)中解决死锁的核心理论依据:

1. 互斥条件

临界资源(同一时间只能被一个线程/进程使用的资源,比如C语言中的全局变量、文件句柄、设备资源、互斥锁pthread_mutex_t)进行排他性占有

  • 例:线程A加锁了mutex1,在A解锁前,线程B绝对无法获取mutex1,这就是互斥。

2. 持有并等待条件

一个线程/进程已经持有了至少一个资源(锁),在不释放已持有资源的前提下,继续等待申请其他资源(锁)

  • 例:C语言中线程A先pthread_mutex_lock(&mutex1)成功(持有mutex1),不解锁,又执行pthread_mutex_lock(&mutex2),此时mutex2被占用,线程A就进入“持有+等待”状态。

3. 不可剥夺条件

线程/进程已经持有的资源(锁)不能被其他线程/进程强行剥夺,只能由持有资源的线程/进程主动释放(解锁)。

  • 例:线程A持有mutex1且未解锁,线程B无法强制抢走mutex1的锁,只能等待A解锁,这是锁的核心特性,也是死锁的关键条件之一。

4. 循环等待条件

多个线程/进程之间,形成了首尾相接的资源申请环路,每个线程都在等待下一个线程持有的资源,无限循环,永远无法满足。

  • 例(C语言经典场景):

线程1:先锁mutexA → 再申请锁mutexB

线程2:先锁mutexB → 再申请锁mutexA

此时线程1持有mutexAmutexB,线程2持有mutexBmutexA,形成环路,死锁必现。


二、C语言编程中 避免/解决死锁的常用有效方法

C语言中死锁主要发生在多线程编程(pthread库) 场景,少量发生在多进程IPC通信场景,所有方法的本质都是:破坏死锁的四个必要条件之一,方法从易到难,优先级由高到低,优先使用前4种基础方法,足够解决99%的死锁场景。

方法1:统一所有线程的资源(锁)申请顺序

核心:破坏「循环等待条件」,彻底杜绝环路产生,C语言中最推荐的方案,实现简单、无副作用。

原理

约定一个全局固定的锁申请顺序,所有线程无论业务逻辑如何,都严格按照这个顺序加锁,解锁顺序无要求(建议和加锁相反)。

C语言代码示例
代码语言:javascript
复制
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 定义两个互斥锁,约定申请顺序:永远先申请mutex1,再申请mutex2
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;

// 线程1:按顺序 锁1 → 锁2
void* thread1_func(void* arg) {
    while(1) {
        pthread_mutex_lock(&mutex1);
        pthread_mutex_lock(&mutex2);
        printf("线程1:成功获取两个锁,执行临界区逻辑\n");
        // 解锁,顺序随意
        pthread_mutex_unlock(&mutex2);
        pthread_mutex_unlock(&mutex1);
        sleep(1);
    }
    returnNULL;
}

// 线程2:同样按顺序 锁1 → 锁2
void* thread2_func(void* arg) {
    while(1) {
        pthread_mutex_lock(&mutex1);
        pthread_mutex_lock(&mutex2);
        printf("线程2:成功获取两个锁,执行临界区逻辑\n");
        pthread_mutex_unlock(&mutex2);
        pthread_mutex_unlock(&mutex1);
        sleep(1);
    }
    returnNULL;
}

int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, thread1_func, NULL);
    pthread_create(&t2, NULL, thread2_func, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    return0;
}

效果:永远不会出现循环等待,彻底避免死锁。

方法2:一次性申请所有需要的资源(锁),要么全拿到,要么全不拿

核心:破坏「持有并等待条件」,因为线程不会“持有部分锁、等待另一部分锁”,要么零持有,要么全持有。

原理

线程在执行临界区逻辑前,一次性申请本次业务需要的所有锁,不分步申请;如果其中任意一个锁申请失败,就释放已经申请到的所有锁,重新等待后再尝试。

  • 核心逻辑:不持有任何锁时才去申请锁,持有锁时不再申请新锁
C语言代码示例
代码语言:javascript
复制
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;

// 封装:一次性申请两个锁,失败则释放已获取的锁
int lock_all(pthread_mutex_t *m1, pthread_mutex_t *m2) {
    int ret1 = pthread_mutex_lock(m1);
    if (ret1 != 0) return-1;
    int ret2 = pthread_mutex_lock(m2);
    if (ret2 != 0) {
        pthread_mutex_unlock(m1); // 申请第二个锁失败,释放第一个
        return-1;
    }
    return0; // 两个锁全部申请成功
}

// 封装:一次性释放所有锁
void unlock_all(pthread_mutex_t *m1, pthread_mutex_t *m2) {
    pthread_mutex_unlock(m2);
    pthread_mutex_unlock(m1);
}

void* thread_func(void* arg) {
    while(1) {
        // 一次性申请,失败则重试
        while (lock_all(&mutex1, &mutex2) != 0);
        printf("线程:成功获取所有锁,执行逻辑\n");
        unlock_all(&mutex1, &mutex2);
        sleep(1);
    }
    returnNULL;
}

int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, thread_func, NULL);
    pthread_create(&t2, NULL, thread_func, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    return0;
}

效果:彻底破坏持有并等待条件,无死锁风险。

方法3:为锁的申请设置超时时间【解决“不知道申请顺序”的场景】

核心:破坏「不可剥夺条件」,相当于给“锁的持有权”增加了「超时剥夺」的特性,是C语言pthread库的原生能力。

原理

C语言的pthread库中,除了阻塞式的pthread_mutex_lock()(永久等待锁),还提供了**带超时的锁申请函数 pthread_mutex_timedlock()**。

  • 给每个锁的申请设置一个超时时间(比如100ms);
  • 如果线程在超时时间内没有获取到锁,就认为申请失败;
  • 此时线程会主动释放已经持有的所有锁,然后休眠一段时间后重试,而不是无限等待。
核心API说明
代码语言:javascript
复制
// 带超时的加锁函数,成功返回0,超时/失败返回非0
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, 
                            const struct timespec *restrict abs_timeout);

效果:线程不会无限等待锁,超时后主动放弃,打破“不可剥夺+无限等待”的死锁闭环,是解决未知锁顺序场景的最优解。

方法4:尽量减少锁的持有时间,缩小临界区范围

核心:间接降低死锁概率,同时提升程序并发性能,是C语言多线程编程的最佳实践

原理

死锁的发生需要“多个线程同时持有部分锁、等待其他锁”,如果线程持有锁的时间极短,申请多个锁的窗口期就会变得极小,死锁发生的概率会无限趋近于0。

  • 具体要求:锁只包裹「必须加锁的临界区代码」,不要把无关的逻辑(比如打印、sleep、IO操作)放到锁内。
错误写法(锁持有时间过长)
代码语言:javascript
复制
pthread_mutex_lock(&mutex1);
printf("执行逻辑1\n");
sleep(3); // 无关的sleep,持有锁3秒
pthread_mutex_lock(&mutex2);
// 临界区逻辑
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
正确写法(缩小临界区)
代码语言:javascript
复制
printf("执行逻辑1\n");
sleep(3); // 无关逻辑放锁外
pthread_mutex_lock(&mutex1);
pthread_mutex_lock(&mutex2);
// 仅临界区代码加锁
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);

方法5:采用资源的剥夺式策略

核心:破坏「不可剥夺条件」,主动“抢占”其他线程持有的锁。

原理

在C语言中可以通过「线程优先级+信号量」实现:给线程设置不同的优先级,高优先级线程申请锁失败时,可以向持有锁的低优先级线程发送信号,让低优先级线程主动释放锁,高优先级线程用完后再归还。

  • 注意:该方法实现复杂,容易导致“优先级反转”,非必要不使用,仅适用于嵌入式/实时系统等特殊场景。

方法6:避免使用嵌套锁(最小化锁的使用)

核心:釜底抽薪,减少死锁的发生场景。

原理

死锁的核心诱因是「多锁嵌套申请」,如果能在C语言编程中:

  1. 尽量用单锁解决问题,减少多锁的使用;
  2. 坚决避免锁的嵌套(比如:锁A内申请锁B,锁B内申请锁C); 就能从根源上降低死锁概率。
  • 最佳实践:能用原子操作(stdatomic.h)代替互斥锁的场景,优先用原子操作,无锁就无死锁。

三、补充:死锁的「检测与恢复」

以上所有方法都是主动避免死锁,如果业务场景复杂,无法提前规避,可以采用「死锁检测+恢复」的被动方案(C语言适用),属于兜底策略:

1. 死锁检测

通过维护「线程-锁」的持有/等待关系表,定期检查是否存在循环等待的环路,如果存在,判定为死锁。

2. 死锁恢复(3种常用手段)

  1. 终止线程:强制终止环路中的一个/多个线程,释放其持有的锁(最简单,C语言中用pthread_cancel()实现);
  2. 资源剥夺:让持有锁的线程主动释放锁,给等待的线程使用;
  3. 重启程序:极端场景下,检测到死锁后直接重启程序(适用于对稳定性要求不高的场景)。

总结

死锁四大必要条件

  1. 互斥条件:资源排他性占用;
  2. 持有并等待条件:持有部分资源,等待其他资源;
  3. 不可剥夺条件:资源不能被强行抢占,只能主动释放;
  4. 循环等待条件:多线程形成首尾相接的资源申请环路。

C语言避免死锁的核心优先级

  1. 统一锁的申请顺序 → 最优解,必用;
  2. 一次性申请所有锁 → 次优解,推荐;
  3. 为锁设置超时时间 → 解决未知顺序场景;
  4. 缩小锁的持有时间 → 最佳实践,辅助优化;
  5. 减少锁的嵌套使用 → 根源降风险。

一句话记忆:破四条件,定顺序,一次申,加超时,缩范围

--完--

读到这里说明你喜欢本公众号的文章,欢迎 置顶(标星)本公众号 C语言中文社区,这样就可以第一时间获取推送了~

在本公众号,后台回复:1024 ,免费领取一份C语言学习大礼包

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-01-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 C语言中文社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 正文
    • 一、死锁的四个必要条件
      • 1. 互斥条件
      • 2. 持有并等待条件
      • 3. 不可剥夺条件
      • 4. 循环等待条件
    • 二、C语言编程中 避免/解决死锁的常用有效方法
      • 方法1:统一所有线程的资源(锁)申请顺序
      • 方法2:一次性申请所有需要的资源(锁),要么全拿到,要么全不拿
      • 方法3:为锁的申请设置超时时间【解决“不知道申请顺序”的场景】
      • 方法4:尽量减少锁的持有时间,缩小临界区范围
      • 方法5:采用资源的剥夺式策略
      • 方法6:避免使用嵌套锁(最小化锁的使用)
    • 三、补充:死锁的「检测与恢复」
      • 1. 死锁检测
      • 2. 死锁恢复(3种常用手段)
    • 总结
      • 死锁四大必要条件
      • C语言避免死锁的核心优先级
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档