news 2026/2/26 6:28:21

Linux学习日记20:死锁

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux学习日记20:死锁

一、前言

前面我们学习了线程同步的概念和互斥锁的适用,本次我们来学习死锁的相关知识。

二、死锁

2.1、死锁的定义

死锁是指多个线程或者进程因竞争共享资源(如互斥锁),互相等待对方释放资源,导致所有线程都陷入 “永久阻塞” 的状态,且无外力干预无法自行解除。举个通俗点的例子就是,线程 A 持有锁 1,等待获取锁 2;线程 B 持有锁 2,等待获取锁 1;两者都不释放已持有的锁,互相等待,程序彻底卡死。如下图所示:

2.2、死锁的必要条件

死锁的发生必须同时满足以下 4 个条件,只要打破其中任意一个,死锁就不会发生:

必要条件通俗解释
互斥条件资源(如互斥锁)只能被一个线程持有,其他线程无法共享(互斥锁的核心特性)
占有且等待条件线程持有一个资源的同时,主动请求获取另一个资源(不释放已持有的资源)
不可抢占条件线程持有的资源不能被强制剥夺,只能由线程主动释放(互斥锁无 “强制解锁” 接口)
循环等待条件多个线程形成 “资源请求闭环”(如 A 等 B 的资源,B 等 C 的资源,C 等 A 的资源)

2.3、典型示例

1、自己锁自己

输入以下代码:

#include <stdio.h> #include <pthread.h> #include <unistd.h> int number; pthread_mutex_t mutex; void *myfun1(void *arg) { for(int i=0;i<10000;i++) { //lock pthread_mutex_lock(&mutex);//两把锁 pthread_mutex_lock(&mutex); int ret; ret = number; ret++; number = ret; printf("fun1 is %ld,number is %d\n",pthread_self(),number); //ulock pthread_mutex_unlock(&mutex); usleep(10); } } void *myfun2(void *arg) { for(int i=0;i<10000;i++) { //lock pthread_mutex_lock(&mutex);//两把锁 pthread_mutex_lock(&mutex); int ret; ret = number; ret++; number = ret; printf("fun2 is %ld,number is %d\n",pthread_self(),number); //ulock pthread_mutex_unlock(&mutex); usleep(10); } } int main() { //init mutex pthread_mutex_init(&mutex,NULL); pthread_t pthid1; pthread_t pthid2; pthread_create(&pthid1,NULL,myfun1,NULL); pthread_create(&pthid2,NULL,myfun2,NULL); pthread_join(pthid1,NULL); pthread_join(pthid2,NULL); //kill mutex pthread_mutex_destroy(&mutex); return 0; }

适用gcc编译器进行编译,运行结果如下:

可以发现什么东西都没有打印出来,这就是死锁了,普通互斥锁的核心规则是:同一线程不能对同一个互斥锁重复加锁—— 第一次加锁后,锁的「持有者」是当前线程,锁状态为「已锁定」;当线程再次调用pthread_mutex_lock时,会阻塞等待锁被释放,但锁的持有者正是自己,因此线程会永久阻塞(死锁),无法继续执行后续代码。

2、交叉加锁

输入以下代码:

#include <stdio.h> #include <pthread.h> #include <unistd.h> pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER; // 线程1:先加锁1,再尝试加锁2 void *thread1(void *arg) { pthread_mutex_lock(&mutex1); printf("线程1持有锁1,等待锁2\n"); sleep(1); // 故意让出CPU,让线程2持有锁2 pthread_mutex_lock(&mutex2); // 阻塞,等待线程2释放锁2 // 临界区(不会执行到) printf("线程1获取所有锁\n"); pthread_mutex_unlock(&mutex2); pthread_mutex_unlock(&mutex1); return NULL; } // 线程2:先加锁2,再尝试加锁1 void *thread2(void *arg) { pthread_mutex_lock(&mutex2); printf("线程2持有锁2,等待锁1\n"); sleep(1); // 故意让出CPU,让线程1持有锁1 pthread_mutex_lock(&mutex1); // 阻塞,等待线程1释放锁1 // 临界区(不会执行到) printf("线程2获取所有锁\n"); pthread_mutex_unlock(&mutex1); pthread_mutex_unlock(&mutex2); return NULL; } int main() { pthread_t tid1, tid2; pthread_create(&tid1, NULL, thread1, NULL); pthread_create(&tid2, NULL, thread2, NULL); pthread_join(tid1, NULL); pthread_join(tid2, NULL); return 0; }

编译并运行,运行结果如下:

执行结果是两个线程互相等待,程序卡死,无后续输出。

2.4、如何避免死锁

1、固定加锁顺序

给所有锁资源分配唯一编号,所有线程必须先加小编号的锁,再加大号的锁,彻底避免 “你等我、我等你” 的闭环。

比如通过下面的代码来修复交叉加锁:

#include <pthread.h> #include <stdio.h> // 步骤1:给锁编号(mutex1=1,mutex2=2,必须先加1再加2) pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER; // 线程1:按“mutex1 → mutex2”加锁 void *thread1(void *arg) { pthread_mutex_lock(&mutex1); // 先加小编号锁 printf("线程1持有mutex1,等待mutex2\n"); pthread_mutex_lock(&mutex2); // 再加大号锁 // 临界区操作 printf("线程1获取所有锁,执行临界区\n"); // 解锁顺序:先解大号锁,再解小编号锁(逆序) pthread_mutex_unlock(&mutex2); pthread_mutex_unlock(&mutex1); return NULL; } // 线程2:严格遵守同一顺序(mutex1 → mutex2) void *thread2(void *arg) { pthread_mutex_lock(&mutex1); // 先加小编号锁(关键!不再先加mutex2) printf("线程2持有mutex1,等待mutex2\n"); pthread_mutex_lock(&mutex2); // 再加大号锁 // 临界区操作 printf("线程2获取所有锁,执行临界区\n"); pthread_mutex_unlock(&mutex2); pthread_mutex_unlock(&mutex1); return NULL; } int main() { pthread_t tid1, tid2; pthread_create(&tid1, NULL, thread1, NULL); pthread_create(&tid2, NULL, thread2, NULL); pthread_join(tid1, NULL); pthread_join(tid2, NULL); return 0; }

运行结果如下:

这样就修复了交叉加锁的问题。

2、一次性获取所有锁

线程在执行临界区前,尝试一次性获取所有需要的锁;如果有任何一个锁拿不到,就释放已拿到的所有锁,重试(而非 “拿着一个等另一个”)。如下代码所示:

#include <stdio.h> #include <pthread.h> #include <unistd.h> // 两个需要同时获取的锁 pthread_mutex_t m1 = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t m2 = PTHREAD_MUTEX_INITIALIZER; // 一次性获取两个锁(核心逻辑) int lock_both(pthread_mutex_t *a, pthread_mutex_t *b) { while (1) { // 1. 非阻塞尝试加锁a if (pthread_mutex_trylock(a) != 0) { usleep(10); continue; } // 2. 非阻塞尝试加锁b,失败则释放a if (pthread_mutex_trylock(b) == 0) return 0; // 成功拿到两个锁 pthread_mutex_unlock(a); // 释放已拿到的a,避免占有且等待 usleep(10); // 重试前休眠,降低CPU占用 } } // 线程函数:演示一次性加锁操作 void *thread_func(void *arg) { int id = *(int *)arg; // 一次性获取m1和m2(避免交叉加锁死锁) lock_both(&m1, &m2); // 临界区:操作共享资源 printf("线程%d:同时拿到m1和m2,执行临界区\n", id); // 解锁(成对释放) pthread_mutex_unlock(&m2); pthread_mutex_unlock(&m1); return NULL; } int main() { int id1 = 1, id2 = 2; pthread_t t1, t2; // 创建两个线程,模拟并发加锁 pthread_create(&t1, NULL, thread_func, &id1); pthread_create(&t2, NULL, thread_func, &id2); // 等待线程结束 pthread_join(t1, NULL); pthread_join(t2, NULL); // 销毁锁 pthread_mutex_destroy(&m1); pthread_mutex_destroy(&m2); return 0; }

编译运行,结果如下:

可以看到这样也可以执行,不会死锁。

3、适用trylock回退重试

如两个线程需要同时操作m1和 m2两个锁,若用普通 pthread_mutex_lock 交叉加锁会死锁;用trylock非阻塞尝试,失败则释放已拿的锁,重试即可避免。 代码如下所示:

#include <stdio.h> #include <pthread.h> #include <unistd.h> // 两个共享锁 pthread_mutex_t m1 = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t m2 = PTHREAD_MUTEX_INITIALIZER; // 线程函数:用trylock避免死锁 void *thread_func(void *arg) { int id = *(int *)arg; int ret1, ret2; while (1) { // 1. 非阻塞尝试加锁m1(trylock:拿不到立即返回EBUSY,不阻塞) ret1 = pthread_mutex_trylock(&m1); if (ret1 != 0) { usleep(10); // 拿不到m1,短暂休眠后重试 continue; } // 2. 非阻塞尝试加锁m2 ret2 = pthread_mutex_trylock(&m2); if (ret2 == 0) { // 成功拿到两个锁,执行临界区 printf("线程%d:成功拿到m1+m2,执行操作\n", id); // 解锁(成对释放) pthread_mutex_unlock(&m2); pthread_mutex_unlock(&m1); break; // 完成操作,退出循环 } else { // 拿到m1但没拿到m2 → 释放m1,避免“占有且等待”(死锁条件) pthread_mutex_unlock(&m1); usleep(10); // 重试前休眠,降低CPU占用 } } return NULL; } int main() { int id1 = 1, id2 = 2; pthread_t t1, t2; // 创建两个线程,模拟并发请求锁(交叉加锁场景) pthread_create(&t1, NULL, thread_func, &id1); pthread_create(&t2, NULL, thread_func, &id2); // 等待线程结束 pthread_join(t1, NULL); pthread_join(t2, NULL); // 销毁锁 pthread_mutex_destroy(&m1); pthread_mutex_destroy(&m2); return 0; }

编译并运行,结果如下:

可以看到这样也可以执行,不会死锁。

2.5、死锁的注意事项

1、死锁一旦发生,程序无法自行恢复,只能重启;

2、即使加锁顺序正确,若锁持有时间过长(如锁内调用sleep/read),死锁概率会大幅增加;

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/23 12:03:57

AutoGPT技术揭秘:大语言模型如何成为自主任务驱动智能体?

AutoGPT技术揭秘&#xff1a;大语言模型如何成为自主任务驱动智能体&#xff1f; 在当今AI快速演进的浪潮中&#xff0c;一个根本性转变正在悄然发生——我们不再只是向机器提问“怎么做”&#xff0c;而是直接告诉它“我要什么”。这种从指令驱动到目标驱动的跃迁&#xff0c;…

作者头像 李华
网站建设 2026/2/25 0:15:40

18、Docker生态系统工具全解析

Docker生态系统工具全解析 在当今的软件开发和部署中,Docker 已经成为了一个不可或缺的工具。它提供了容器化技术,使得应用的部署和管理变得更加高效和便捷。而围绕 Docker 也诞生了一系列的生态系统工具,这些工具可以帮助我们更好地使用 Docker,提高开发和部署的效率。本…

作者头像 李华
网站建设 2026/2/24 11:04:29

25、容器监控与应用实践全解析

容器监控与应用实践全解析 1. 容器监控工具介绍 1.1 使用 Collectd 可视化容器指标 Collectd 可用于获取所有运行容器的统计信息。对于名为 cpu_stats 的统计信息,它会将 PUTVAL 字符串写入标准输出,该字符串可被 Collectd 理解并发送到 Graphite 数据存储(即 Carbon…

作者头像 李华
网站建设 2026/2/22 9:00:34

AutoGPT如何识别任务优先级?重要紧急四象限法应用

AutoGPT如何识别任务优先级&#xff1f;重要紧急四象限法应用 在当前AI技术快速演进的背景下&#xff0c;我们正见证一个关键转变&#xff1a;智能体从“听令行事”的工具&#xff0c;逐步成长为能够自主思考、规划并执行复杂目标的数字代理。以AutoGPT为代表的自主智能体&…

作者头像 李华
网站建设 2026/2/25 10:09:34

收藏这篇就够了!RAG技术详解:让大模型告别幻觉的终极方案

本文介绍RAG技术如何解决大模型"幻觉"问题&#xff0c;详细阐述其核心组件&#xff08;知识嵌入、检索器、生成器&#xff09;及工作原理。系统介绍《大模型应用开发&#xff1a;RAG实战课》一书内容框架&#xff0c;包括系统构建、优化、评估和复杂范式。该书以实战…

作者头像 李华
网站建设 2026/2/26 4:53:24

25、深入理解 Java 中的严格性与惰性

深入理解 Java 中的严格性与惰性 1. 严格性与惰性的基本概念 在编程中,严格性和惰性不仅仅适用于方法参数,而是涉及到编程的方方面面。以 Java 为例,Java 是一种严格的语言,这意味着表达式会立即求值。 例如下面的代码: int x = 2 + 3;这里, x 会立即被求值为 5,…

作者头像 李华