Linux 线程同步与互斥(六) 线程安全与重入问题,死锁,线程done

作者:codeacac日期:2026/4/30

目录

一、线程安全与重入问题

概念

线程安全

重入

多线程重入函数

信号导致的重入

可重入与线程安全的联系

可重入与线程安全的区别

二、死锁

概念

造成死锁的4个必要条件

避免死锁的做法:

三、STL, 智能指针和线程安全

四、总结


一、线程安全与重入问题

概念

线程安全

线程安全就是当多个线程同时访问同一块资源(如全局变量、任务队列、打印终端)时,最终结果能符合预期,不会出现数据错乱、逻辑错误,这就是线程安全。

我们可以结合上一篇线程池的代码来理解,任务队列 _tasks 是线程池最核心的共享资源。

不安全场景:如果没有 _mutex 锁保护的话,线程 1 刚把任务 push 进去,任务队列结构还没稳定,线程 2 就进来 pop,会导致队列链表断裂、数据覆盖,程序直接崩溃。

**安全场景:**我们代码里所有对 _tasks 的操作(push、pop、empty)都进行了加锁和解锁。同一时刻只有一个线程能操作队列,所以线程是安全的。

重入

重入这个概念稍微抽象一点,它描述的是函数被调用的状态。同一个函数,被不同的执行流打断,并且在还没执行完的时候就被再次进入。

重入分为两种情景 : 1. 多线程重入函数,2. 信号导致一个执行流重复进入函数

多线程重入函数

我们先来看看第一种情况

在我们的线程池代码中,HandlerTask 函数被线程 1、线程 2、线程 3、线程 4 同时执行。
假设线程 1 正在执行 HandlerTask 里的取任务逻辑,此时时间片到了,操作系统切走线程 1,换线程 2 进入 HandlerTask。这就是多线程的重入。

这里我们强调一下可重入函数和不可重入函数:

  • 可重入函数:函数内部只使用局部变量(栈上的变量),不管多少个执行流同时进来,结果都正确。
  • 不可重入函数:函数内部使用了全局变量或静态变量,且没有锁保护。如果一个执行流还没改完变量,另一个进来改了,第一个流再继续执行时,变量值就错了。

我们线程池代码是安全的:虽然发生了多线程重入,但因为我们对共享资源(队列)加了锁,所以它是 “安全的重入”。如果不加锁,就是 “危险的重入”。

信号导致的重入

信号导致的重入是 Linux 系统编程里更底层的重入。

举个例子一个线程正在运行,突然收到一个信号(比如 SIGINT 中断),操作系统暂停当前线程,去执行信号处理函数。如果信号处理函数里恰好调用了某个普通函数,而这个普通函数刚好正在被主逻辑执行,这就叫信号重入。

危险的地方就是信号处理函数和主逻辑可能会同时修改同一个全局变量,导致数据竞争。

可重入和不可重入,哪个更好?

可重入更好,因为可重入就代表着同一个函数,被多个线程多次调用后,也不会出错、不会乱、不会崩。不可重入代表当一个线程在执行某个函数时,其余的线程一律不能执行这个函数,如果其他线程也执行的话,就会出现问题、容易出 bug、多线程时绝对不能乱用。

所以可重入函数的特点可以总结为以下几点 :

  • 不用全局变量
  • 不用静态变量
  • 不操作共享资源
  • 或者操作共享资源时加锁
  • 只使用自己的局部变量

我们的线程池代码中的 HandlerTask 函数就是标准的:可重入 + 线程安全 函数。因为多线程环境下不会崩溃,我们的线程池中的 4 个线程同时调用 HandlerTask 时就是可重入的,也就代表着怎么调用 HandlerTask 都不会出错。

可重入与线程安全的联系

可重入与线程安全的区别

注意:

二、死锁

概念

死锁是多线程/多进程并发场景下的致命 bug,指的是两个或多个线程,各自拿着对方需要的锁,同时又在等待对方手里的锁,谁都不肯先释放自己的锁,最终所有线程都陷入永久阻塞、程序卡死、再也跑不下去的状态。

如下图所示,为了方便表述,假设现在线程 A,线程 B 必须同时持有锁 1 和锁 2,才能进行后续资源的访问:

我们知道申请一把锁是原子的,但是申请两把锁就不是原子的了。

此时线程A自己持有锁1,但是它还想要线程B持有的锁2,线程B也是同理。

所以最终造成的结果就是死锁,谁都不肯先释放自己的锁,最终所有线程都陷入永久阻塞、程序卡死、再也跑不下去的状态。

造成死锁的4个必要条件

1. 互斥条件:一个资源只能被一个线程持有,不能共享。

2. 请求与保持条件:线程已经持有了一把锁,还不释放,并且同时还要去去申请另一把锁。

3. 不可剥夺条件:线程持有的锁,不能被其他线程强行抢走,只能自己主动释放。

4. 循环等待条件:多个线程形成一个循环等待链,每个线程都在等下一个线程手里的资源。(A 等 B,B 等 A)

因此避免死锁的方法就是只要破坏其中任意一个条件,死锁就不会发生。

避免死锁的做法:

怎么避免死锁?

方案 1:统一锁的获取顺序
让所有线程,都按照完全相同的顺序抢锁:比如永远先抢锁 1,再抢锁 2。这样就不会出现 A 拿 1 等 2、B 拿 2 等 1 的情况,从根源避免循环等待。

方案 2:一次性申请所有锁(原子性获取)
用 std::lock 一次性同时抢多把锁,要么全抢到,要么一把都不抢,不会出现拿一半等一半的情况。

方案 3:避免持有锁的同时申请其他锁
尽量让线程拿一把锁,用完释放后,再拿下一把,不要在持有锁的代码里去抢其他锁。

方案 4:使用带超时的锁
比如std::timed_mutex,抢锁超时就主动释放自己手里的锁,重试,避免永久阻塞。

三、STL, 智能指针和线程安全

STL 中的容器是否是线程安全的?

不是。原因是 STL 的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响。而且对于不同的容器,加锁方式的不同,性能可能也不同 (例如 hash 表的锁表和锁桶)。因此 STL 默认不是线程安全。如果需要在多线程环境下使用,往往需要调用者自行保证线程安全。

智能指针是否是线程安全的?

对于 unique_ptr, 由于只是在当前代码块范围内生效,因此不涉及线程安全问题。

对于 shared_ptr, 多个对象需要共用一个引用计数变量,所以会存在线程安全问题。但是标准库实现的时候考虑到了这个问题,基于原子操作 (CAS) 的方式保证 shared_ptr 能够高效,原子的操作引用计数。

四、总结

本文探讨了多线程编程中的核心问题:线程安全与重入、死锁机制以及STL和智能指针的线程安全性。线程安全指多线程访问共享资源时结果的正确性,通过加锁保护实现;重入分为多线程和信号重入,可重入函数通过避免全局变量和加锁确保安全。死锁由四个必要条件引发,可通过统一锁顺序、原子获取锁等方式避免。STL容器默认非线程安全,需自行加锁;智能指针中shared_ptr通过原子操作保证线程安全。文章为多线程开发提供了关键概念和解决方案。

谢谢大家的观看!


Linux 线程同步与互斥(六) 线程安全与重入问题,死锁,线程done》 是转载文章,点击查看原文


相关推荐


把 Git 提交历史变成一条流动的河——Project River
仿生狮子2026/4/22

是什么 你有没有好奇过一个开源项目十年的贡献者活动长什么样?谁一直在写代码?谁是后来加入的?版本大升级时社区发生了什么变化? 我做了 Project River,一个 Git 历史可视化工具——输入一个 Git 仓库,能把每位贡献者的提交活动渲染成随时间流动的河流图(Streamgraph)。 项目地址:github.com/Lionad-Moro… 在线体验:lionad-morotar.github.io/project-riv… 直接看效果: 河流越宽,说明当天的提交越多。每条色带


React性能优化
whuhewei2026/4/13

React应用在复杂场景下容易出现渲染性能瓶颈,合理优化能显著提升用户体验。React性能优化手段的核心在于减少不必要的渲染、控制资源加载和合理使用缓存机制。 1. 使用 React.memo 避免子组件无意义重渲染 当父组件更新时,即使子组件props未变,也会默认重新渲染。React.memo可缓存组件输出,仅在props变化时重新更新。 示例Demo: import React, { useState } from "react"; const ExpensiveComponen


PHP $_GET 变量详解
froginwe112026/4/5

PHP $_GET 变量详解 引言 PHP $_GET 变量是 PHP 中用于处理 URL 查询字符串参数的一个内置数组。在 Web 开发中,$_GET 变量经常用于收集来自表单的数据或者从 URL 中提取信息。本文将详细介绍 PHP $_GET 变量的基本用法、操作方法和注意事项。 一、$_GET 变量简介 在 PHP 中,$_GET 是一个超级全局变量,用于存储通过 URL 传递的参数。这些参数以名值对的形式出现在 URL 中,如 http://www.example.com/?ke


go实战案例:如何基于 Consul 给微服务添加服务注册与发现?
五年小兵勇闯互联网2026/3/27

在单体应用向微服务架构演进的过程中,原本的巨石型应用会按照业务需求被拆分成多个微服务,每个微服务会提供特定的功能,并可能依赖于其他的微服务。每个微服务实例都可以动态部署,服务实例之间的调用通过轻量级的远程调用方式(HTTP、消息队列等)实现,它们之间通过预先定义好的接口进行访问。         由于服务实例是动态部署的,每个服务实例的地址和服务信息都可能动态变化,这就势必需要一个中心化的组件对各个服务实例的信息进行管理,该组件管理了各个部署好的服务实例元数据,包括服务名、IP地址、端口号、服务


VMware虚拟机CentOS磁盘扩容完整指南(解决growpart报错 & LVM扩容)
Microi风闲2026/3/19

文章目录 前言✨一、环境与背景二、第一阶段:VMware 层面扩容三、第二阶段:CentOS 系统内部扩容方法一:标准LVM扩容流程(推荐)方法二:解决 growpart 报错方案(备用) 四、总结与注意事项 前言✨ 在日常开发和运维中,我们经常遇到 VMware 虚拟机磁盘空间不足的问题。本文记录了如何为一台正在运行的 CentOS 7 虚拟机安全地扩容磁盘空间的全过程。本次操作不仅涵盖了标准的扩容步骤,还重点解决了实际操作中可能遇到的两个关键问题: growpart


OpenClaw实战-NAS配置从0到1详细教程及踩坑记录
可夫小子2026/3/11

💡 大家好,我是可夫小子,关注AI编程、AI自动化和自媒体。 背景 我去年在自己Macbook上就已经安装了Openclaw了,当时基本就是一行命令,比较顺利。经过这两个月,Openclaw版本经历了很多更新,当时的安装教程有些过时了。最近,我需要在一台群晖的Nas部署Openclaw,硬是花了两天时间才完成。我把整个详细的部署过程,一些踩坑经历用图文记录下来,就有这篇包含30多张截图的图文。 方案说明 在Nas安装Openclaw,主要有三种方案来实现 直接通过官方脚本,安装到Nas物


NineData 迁移评估功能正式上线
NineData2026/3/3

做过数据库迁移的人,应该都有过类似经历:明明方案写得挺好,时间排得也挺满,但心里一直不踏实。因为真正的问题,往往不是工具能不能迁,而是迁过去以后还能不能跑。 NineData 这次做的迁移评估功能,本质上就只做一件事:提前把不确定性拆开,摊在你面前。 NineData 的一份体检报告,把风险摊在桌面上 异构迁移真正需要关注的是:要改多少?哪些能改?哪些根本不能动? 下面来列一列 NineData 能做哪些事情。 一、对象兼容性评估:哪些能直接迁,哪些一定要改 系统会自动分析源库和目标库的对象差异


AI 系统架构
lizhongxuan2026/2/23

AI 系统看起来很复杂,但核心可以压缩成三句话: 尽量少搬数据:很多时候不是算不动,而是数据搬运太慢。 尽量提高有效计算密度:让硬件更多时间在做有价值的乘加计算。 尽量重叠计算与通信:训练和推理都要避免“设备空等”。 换句话说,AI 性能问题本质上是 计算(Compute)+ 访存(Memory)+ 通信(Communication) 的协同问题。 1. AI 系统栈 层级主要职责典型问


Flutter三方库适配OpenHarmony【apple_product_name】异步调用与错误处理
淼学派对2026/2/14

前言 欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net 本文将围绕 apple_product_name 的实际 API,从 Future 基础到全局错误兜底,给出一套完整的异步调用与错误处理方案。 先给出结论式摘要: 所有 API 返回 Future:getMachineId()、getProductName()、lookup() 都是异步的,必须 await 或 .then()三类异常要分层捕获:PlatformExc


基于uview-pro的u-dropdown扩展自己的dropdown组件
LC同学479812026/2/6

基于uview-pro的u-dropdown扩展自己的dropdown组件 uview-pro的u-dropdown只能是菜单,且只能向下展开,当前组件采用它的核心逻辑,去除多余逻辑,兼容上/下展开,以及自定义展示的内容,不再局限于菜单形式 import type { ExtractPropTypes, PropType } from 'vue'; import { baseProps } from 'uview-pro/components/common/props'; /** * u-

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 XYZ博客