Linux--线程安全、死锁、单例线程池题目

[复制链接]
发表于 2026-10-3 16:29:07 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×

一、开篇:这一系列讲的是什么

线程系列前面的几篇已经把基础铺过了——线程的初步认识、线程的分离与线程库地址关系、线程的简单封装,再到线程的互斥(临界资源同一时间只能被一个线程使用)、生产者消费者模型、信号量、线程池(含日志日志说明)。这一篇把线程领域里最容易挂人的三块概念一次讲清:线程安全、死锁,以及用银行家算法做死锁避免。

二、线程安全:并发跑对了,数据才不会坏

线程安全指的是:在多线程环境下,某个操作或方法被多个线程并发执行时,仍能保持数据的一致性和完整性——不会因为线程交替执行而导致数据被破坏、或程序行为不符合预期。换句话说,如果一个操作在多线程下能保证数据一致性和操作的原子性,它就是线程安全的。

1. 决定它成不成立的三个因素

  • 原子性:操作要么全部完成,要么完全不执行,不会被其他线程的》《操作打断;
  • 可见性:一个线程对共享变量的修改,能立刻被其他线程看到(否则会读到旧值);
  • 有序性:程序按代码的先后顺序执行,不会因指令重排序导致数据错乱。

这三条在 C/C++ 里对应到实践上就是:互斥锁保原子性、条件变量/信号量保同步、内存屏障或原子类型保可见性与顺序。

2. 问题从哪里来

  • 共享资源:多个线程访问同一个资源(如全局变量、同一个文件),没有同步机制就会数据不一致;
  • 竞态条件:两个或多个线程同时执行,其结果依赖它们的相对执行速度,导致程序行为不可预测。

一个最经典的例子是「++」不是原子操作:count++ 实际是读、改、写三步,两个线程同时加,最后可能只加了 1。这种 bug 离线跑一万次都正常,一上压测就出现——因为它是竞态,不是必现。

三、死锁:两个进程互相等,谁也动不了

死锁是指两个或多个进程(线程)在执行过程中,因竞争资源或彼此通信而造成的阻塞现象:没有外力作用,它们都将无法推进下去。此时称系统处于死锁状态,这些永远在互相等待的进程(线程)称为死锁进程。

1. 产生原因

  • 竞争不可抢占资源:系统中可供多进程共享的资源(如打印机、公用队列)数量不足以满足各进程需要,于是产生竞争而死等;
  • 竞争可消耗资源:除了不可抢占资源,对可消耗资源(如消息、信号)的争夺同样可能引起死锁;
  • 进程推进顺序不当:运行过程中推进顺序不合理,也会撞出死锁。

2. 四个必要条件(缺一不可)

  • 互斥条件:至少有一个资源被标志为独占,一次只能被一个进程(线程)使用;
  • 请求与保持条件:进程(线程)已经持有至少一个资源,同时又去请求其他进程持有的资源;
  • 不可剥夺条件:已分配的资源不能被强行剥夺,只能由持有者自己释放;
  • 循环等待条件:存在一个进程(线程)的资源请求链,使得每个进程都在等待下一个进程所持有的资源。

这四条是判断「会不会死锁」的检查表。 只要能破坏其中任意一条,死锁的概率就大幅下降——这也是下面预防思路的出发点。

3. 预防方法:逐条破坏条件

  • 破坏互斥条件:某些资源允许多进程同时访问而不用独占,例如用读写锁代替互斥锁(读并发、写独占);
  • 破坏请求与保持条件:申请新资源前先释放已持有的资源,或者拿到全部必要资源后再统一申请;
  • 破坏不可剥夺条件:允许系统强行剥夺某些进程的资源并转给其他等待者;
  • 破坏循环等待条件:对资源全局排序、按顺序申请,杜绝形成环。例如可以用银行家算法来避免死锁。

4. 已经死锁了怎么办:三种解除手段

  • 资源剥夺法:挂起某些死锁进程,抢占其资源分配给其他死锁进程;
  • 撤销进程法:强制撤销部分或全部死锁进程并剥夺其资源,直到打破环路。撤销原则可以按进程优先级和撤销代价权衡;
  • 进程回退法:让一个或多个进程回退到足以避免死锁的状态,回退时自愿释放资源而非被剥夺。这要求系统保存进程历史信息、设置还原点。

工程上最常用的是第 2 条里的「超时 + 重启进程」,但代价是服务中断;所以真正的水平在「别让它发生」——也就是下节的银行家算法。

四、银行家算法:事前算一笔账,别让它死

银行家算法的核心思想:在系统中,对每种资源都设定一个最大需求量和当前可用量;当进程提出资源请求时,系统先检查该进程是否满足其最大需求量限制,若请求合法,则尝试分配并检验「分配后系统是否仍安全(不进入不安全状态)」;若安全就分配,否则拒绝请求。

翻译成人话,就是每次放款前先推演一遍:如果把资源给出去,还有没有可能找到一种顺序让所有进程都顺利跑完?有,就放;没有,就不放。

1. 算法步骤

  • 检查请求是否合法:请求向量 Request[i] 是否超过该进程已声明的 Need[i](最大需求);若超过,说明非法,直接拒绝;
  • 检查系统是否有余量:Request[i] 是否超过当前 Available;超过则进程需等待;
  • 试探性分配:把请求从 Available 扣掉、加到进程的 Allocation 上、并相应减少 Need;
  • 安全性检查:用剩余资源推算是否存在一个进程完成序列;存在则分配成立,否则回滚这次试探、拒绝请求。

2. 和「死锁预防」的差别

这一点很多人搞混:前面四种预防是「改条件」,代价是资源利用率降低(比如强制按顺序申请会让并发度变窄);银行家算法是「做决策」,在不牺牲并发的前提下动态判断。 所以它是「死锁避免」而不是「死锁预防」——允许互斥和需求存在,只是每次分配前先算账。

3. 在真实系统里的样子

很少有人真去手写银行家算法,但它的判断逻辑可以套在很多资源调度上:

  • 线程池:新任务来了先算「按当前队列剩余容量和已用线程,任务能否在超时前完成」,不能就拒绝或降级,而不是无限建线程把机器拖死;
  • 数据库连接池:这是最贴近的例子——一次事务要拿多个连接时,按固定顺序(比如按连接下标)申请,或者先算「分配后最坏情况是否还能满足所有活动事务」,不满足就等待,这就是银行家算法在业务层的翻版;
  • 多把锁的加锁顺序:全系统约定一个锁序(比如 lockA → lockB → lockC),本质也是破坏循环等待条件的那一条,实现成本最低、收益最大。

五、三块概念的关系,一图收尾

  • 线程安全解决的是「并发下数据不坏」——靠原子性、可见性、有序性;
  • 死锁解决的是「拿到资源后别互相卡死」——靠破坏四个必要条件,或事后剥夺/回退;
  • 银行家算法解决的是「分配资源时别把自己算进死局」——事前试算、不安全就不给。

一句落地建议:日常写代码,先把「一把锁保护一段临界区」写对(线程安全),再立刻给所有锁定一个全局加锁顺序(防循环等待),最后用超时和监控监控兜住残余风险。 这三步做完,死锁基本就从「偶发事故」变成「可观测可观测指标」了。

(本文由 qidao123.com企服评测-应用市场 编辑提示整理:内容面向 Linux 平台上使用 C/C++ pthread 的并发编程学习,适用于线程同步、资源调度与死锁排查的场景梳理。)

回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表