
死锁的发生是缺一不可的,必须同时满足以下4个条件,只要破坏其中任意一个条件,死锁就绝对不会发生,这是C语言(以及所有多线程/多进程编程)中解决死锁的核心理论依据:
对临界资源(同一时间只能被一个线程/进程使用的资源,比如C语言中的全局变量、文件句柄、设备资源、互斥锁pthread_mutex_t)进行排他性占有。
mutex1,在A解锁前,线程B绝对无法获取mutex1,这就是互斥。一个线程/进程已经持有了至少一个资源(锁),在不释放已持有资源的前提下,继续等待申请其他资源(锁)。
pthread_mutex_lock(&mutex1)成功(持有mutex1),不解锁,又执行pthread_mutex_lock(&mutex2),此时mutex2被占用,线程A就进入“持有+等待”状态。线程/进程已经持有的资源(锁),不能被其他线程/进程强行剥夺,只能由持有资源的线程/进程主动释放(解锁)。
mutex1且未解锁,线程B无法强制抢走mutex1的锁,只能等待A解锁,这是锁的核心特性,也是死锁的关键条件之一。多个线程/进程之间,形成了首尾相接的资源申请环路,每个线程都在等待下一个线程持有的资源,无限循环,永远无法满足。
线程1:先锁mutexA → 再申请锁mutexB
线程2:先锁mutexB → 再申请锁mutexA
此时线程1持有mutexA等mutexB,线程2持有mutexB等mutexA,形成环路,死锁必现。
C语言中死锁主要发生在多线程编程(pthread库) 场景,少量发生在多进程IPC通信场景,所有方法的本质都是:破坏死锁的四个必要条件之一,方法从易到难,优先级由高到低,优先使用前4种基础方法,足够解决99%的死锁场景。
核心:破坏「循环等待条件」,彻底杜绝环路产生,C语言中最推荐的方案,实现简单、无副作用。
约定一个全局固定的锁申请顺序,所有线程无论业务逻辑如何,都严格按照这个顺序加锁,解锁顺序无要求(建议和加锁相反)。
#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;
}

效果:永远不会出现循环等待,彻底避免死锁。
核心:破坏「持有并等待条件」,因为线程不会“持有部分锁、等待另一部分锁”,要么零持有,要么全持有。
线程在执行临界区逻辑前,一次性申请本次业务需要的所有锁,不分步申请;如果其中任意一个锁申请失败,就释放已经申请到的所有锁,重新等待后再尝试。
#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;
}

效果:彻底破坏持有并等待条件,无死锁风险。
核心:破坏「不可剥夺条件」,相当于给“锁的持有权”增加了「超时剥夺」的特性,是C语言pthread库的原生能力。
C语言的pthread库中,除了阻塞式的pthread_mutex_lock()(永久等待锁),还提供了**带超时的锁申请函数 pthread_mutex_timedlock()**。
// 带超时的加锁函数,成功返回0,超时/失败返回非0
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
const struct timespec *restrict abs_timeout);
效果:线程不会无限等待锁,超时后主动放弃,打破“不可剥夺+无限等待”的死锁闭环,是解决未知锁顺序场景的最优解。
核心:间接降低死锁概率,同时提升程序并发性能,是C语言多线程编程的最佳实践。
死锁的发生需要“多个线程同时持有部分锁、等待其他锁”,如果线程持有锁的时间极短,申请多个锁的窗口期就会变得极小,死锁发生的概率会无限趋近于0。
pthread_mutex_lock(&mutex1);
printf("执行逻辑1\n");
sleep(3); // 无关的sleep,持有锁3秒
pthread_mutex_lock(&mutex2);
// 临界区逻辑
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
printf("执行逻辑1\n");
sleep(3); // 无关逻辑放锁外
pthread_mutex_lock(&mutex1);
pthread_mutex_lock(&mutex2);
// 仅临界区代码加锁
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
核心:破坏「不可剥夺条件」,主动“抢占”其他线程持有的锁。
在C语言中可以通过「线程优先级+信号量」实现:给线程设置不同的优先级,高优先级线程申请锁失败时,可以向持有锁的低优先级线程发送信号,让低优先级线程主动释放锁,高优先级线程用完后再归还。
核心:釜底抽薪,减少死锁的发生场景。
死锁的核心诱因是「多锁嵌套申请」,如果能在C语言编程中:
stdatomic.h)代替互斥锁的场景,优先用原子操作,无锁就无死锁。以上所有方法都是主动避免死锁,如果业务场景复杂,无法提前规避,可以采用「死锁检测+恢复」的被动方案(C语言适用),属于兜底策略:
通过维护「线程-锁」的持有/等待关系表,定期检查是否存在循环等待的环路,如果存在,判定为死锁。
pthread_cancel()实现);一句话记忆:破四条件,定顺序,一次申,加超时,缩范围。
--完--
读到这里说明你喜欢本公众号的文章,欢迎 置顶(标星)本公众号 C语言中文社区,这样就可以第一时间获取推送了~
在本公众号,后台回复:1024 ,免费领取一份C语言学习大礼包