【Linux线程】Linux系统多线程(十):线程安全和重入、死锁相关话题

🎬 个人主页艾莉丝努力练剑
专栏传送门 :《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录
Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享

⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平


🎬 艾莉丝的简介:


文章目录

  • [1 ~> 线程安全(Thread Safety)可重入(Reentrant)](#1 ~> 线程安全(Thread Safety)可重入(Reentrant))
    • [1.1 对比表](#1.1 对比表)
    • [1.2 核心逻辑总结](#1.2 核心逻辑总结)
  • [2 ~> 线程安全与重入:不仅仅是"加个锁"那么简单](#2 ~> 线程安全与重入:不仅仅是“加个锁”那么简单)
    • [2.1 什么是线程安全?](#2.1 什么是线程安全?)
    • [2.2 什么是函数重入?](#2.2 什么是函数重入?)
    • [2.3 线程安全 VS 可重入:它们的"血缘关系"](#2.3 线程安全 VS 可重入:它们的“血缘关系”)
      • [2.3.1 线程安全 VS 可重入:关键结论](#2.3.1 线程安全 VS 可重入:关键结论)
      • [2.3.2 生活案例:抢票与电话铃声](#2.3.2 生活案例:抢票与电话铃声)
    • [2.4 哪些坑会导致"不可重入"?](#2.4 哪些坑会导致“不可重入”?)
  • [3 ~> 死锁:两个小朋友的"五毛钱困局"](#3 ~> 死锁:两个小朋友的“五毛钱困局”)
    • [3.1 什么是死锁?](#3.1 什么是死锁?)
      • [3.1.1 生活案例:一块钱的辣条](#3.1.1 生活案例:一块钱的辣条)
    • [3.2 死锁的四个必要条件(缺一不可)](#3.2 死锁的四个必要条件(缺一不可))
  • [4 ~> 避坑指南:如何优雅地预防死锁?](#4 ~> 避坑指南:如何优雅地预防死锁?)
    • [4.1 方案一:规定加锁顺序(破坏循环等待)](#4.1 方案一:规定加锁顺序(破坏循环等待))
    • [4.2 方案二:一次性申请(破坏请求与保持)](#4.2 方案二:一次性申请(破坏请求与保持))
    • [4.3 方案三:非阻塞加锁(使用 trylock)](#4.3 方案三:非阻塞加锁(使用 trylock))
  • [5 ~> STL 容器与智能指针:它们真的安全吗?](#5 ~> STL 容器与智能指针:它们真的安全吗?)
    • [5.1 STL 容器:性能至上,安全自负](#5.1 STL 容器:性能至上,安全自负)
    • [5.2 智能指针:半成品安全](#5.2 智能指针:半成品安全)
  • [6 ~> 我们应该了解的一些"高级锁"概念](#6 ~> 我们应该了解的一些“高级锁”概念)
    • [6.1 悲观锁 VS 乐观锁](#6.1 悲观锁 VS 乐观锁)
    • [6.2 CAS (Compare-And-Swap)](#6.2 CAS (Compare-And-Swap))
    • [6.3 自旋锁 VS 阻塞锁](#6.3 自旋锁 VS 阻塞锁)
      • [6.3.1 生活案例:等朋友下楼](#6.3.1 生活案例:等朋友下楼)
  • [7 ~> 总结与建议](#7 ~> 总结与建议)
    • [7.1 总结](#7.1 总结)
    • [7.2 学习建议](#7.2 学习建议)
  • 结尾

1 ~> 线程安全(Thread Safety)可重入(Reentrant)

1.1 对比表

特性 线程安全 (Thread Safety) 可重入 (Reentrant)
定义 多个线程同时并发访问时,结果始终正确,不会产生竞态条件。 函数被中断并重新进入执行后(如信号处理、递归),结果依然正确。
关注点 关注多线程间共享资源的同步。 关注单线程内多次调用或中断执行的独立性。
实现手段 主要通过加锁 (Lock)、信号量、原子操作来保护共享资源。 不使用 全局变量、静态变量。只使用局部变量、栈数据或常量。
常见场景 高并发服务端开发、共享缓存访问。 信号处理函数 (Signal Handler)、递归调用。
外部依赖 依赖操作系统提供的同步原语(如 Mutex)。 仅依赖自身逻辑,通常不需要锁。
相互关系 线程安全不一定是可重入的(如加锁可能导致重入死锁)。 可重入函数一定是线程安全的。

1.2 核心逻辑总结

  • 线程安全是用"管理共享资源"的手段来解决并发冲突。
  • 可重入是用"不使用共享资源"的手段来从根源消除冲突。

2 ~> 线程安全与重入:不仅仅是"加个锁"那么简单

很多初学者觉得:"只要我给全局变量加了锁,我的代码就是线程安全的了。"

这句话对,但不完全对。

2.1 什么是线程安全?

简单来说,线程安全就是"不出问题"

当多个线程像一群疯狂的食客一样涌向同一个共享资源(比如全局变量、静态变量)时,如果你的代码能够保证逻辑执行结果依然符合预期,不会因为执行流的交错而导致数据错乱,那它就是线程安全的。

2.2 什么是函数重入?

这是一个比线程安全更底层、也更容易被忽视的概念。

重入(Reentrancy) 描述的是函数的属性:同一个函数被不同的执行流同时调用。

  • 多线程重入 :线程 A 正在执行 func,还没跑完,时间片到了,线程 B 又进来跑 func

  • 信号处理重入 :这更坑。线程 A 跑着 func,突然一个系统信号中断了它,跑去执行信号处理函数,而信号处理函数里又调用了同一个 func

如果一个函数在被重入的情况下,运行结果没有任何问题,我们称之为可重入函数

2.3 线程安全 VS 可重入:它们的"血缘关系"

这两者虽然经常被混谈,但在物理层面其实是"风马牛不相及"的:

  • 线程安全描述的是多线程并发时的状态。

  • 函数重入描述的是函数本身的结构特点。

2.3.1 线程安全 VS 可重入:关键结论

可重入函数一定是线程安全的(因为它不依赖任何外部状态,天生免疫并发干扰)。

线程安全函数不一定可重入。

2.3.2 生活案例:抢票与电话铃声

想象你在火车站窗口抢票,为了防止数据出错,售票员进屋前反锁了门(加锁 )。这保证了线程安全

但是,正当售票员处理到一半时,家里的急促电话打进来了(信号中断)。售票员必须在原地接电话,而电话那头的老婆要求他帮她也出一张票。

此时尴尬了:售票员正锁着门在里面,他试图再次进入这个逻辑去出票,结果发现门被"自己"锁上了,死等。

这就是典型的:虽然加了锁保证了线程安全,但因为函数不可重入,导致了死锁。

2.4 哪些坑会导致"不可重入"?

  • 使用了静态或全局变量:这是最常见的。

  • 调用了 malloc/free :因为 malloc 内部维护了一个全局链表来管理堆空间。

  • 使用了标准 I/O 库:很多 I/O 实现都以全局方式使用数据结构。


3 ~> 死锁:两个小朋友的"五毛钱困局"

如果说线程安全是"数据错乱",那死锁就是"整个世界静止了"。

3.1 什么是死锁?

死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象。若无外力作用,它们都将无法推进。

3.1.1 生活案例:一块钱的辣条

有两个小朋友,小明和小红,他们都想买一包价值 1 块钱的辣条。

  • 小明手里有 5 毛钱。

  • 小红手里也有 5 毛钱。

小明对小红说:"你把 5 毛借给我,我买了分你一半。"

小红对小明说:"凭什么?你先给我,我买了分你一半。"

两人互不相让,死死攥着手里的 5 毛钱。结果谁也吃不上辣条,这就是死锁

3.2 死锁的四个必要条件(缺一不可)

要产生死锁,必须同时满足以下四个条件。换句话说,只要你破坏了其中任何一个,死锁就解开了:

1、互斥条件:资源只能被一个线程占用(辣条只能一个人买)。

2、请求与保持条件:线程已经拿到了一个资源,还要去申请另一个,且不释放已有的(攥着 5 毛钱不松手,还想要对方的)。

3、不剥夺条件:线程拿到的资源不能被强制抢走(你不能从对方手里明抢 5 毛钱)。

4、循环等待条件:形成了一个等待环路(A 等 B,B 等 A)。


4 ~> 避坑指南:如何优雅地预防死锁?

作为博主,我总结了三套方案来破坏上述条件:

4.1 方案一:规定加锁顺序(破坏循环等待)

如果所有线程都规定:必须先拿 A 锁,再拿 B 锁。那么就不会出现 A 等 B、B 等 A 的情况。

4.2 方案二:一次性申请(破坏请求与保持)

在 C++11 中,我们可以使用 std::lock 同时锁定多个互斥量:

cpp 复制代码
// 伪代码示例
std::mutex mtx1, mtx2;
// 能够同时锁住两个锁,如果锁不住其中一个,会全部释放重新尝试
std::lock(mtx1, mtx2); 
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);

4.3 方案三:非阻塞加锁(使用 trylock)

如果你申请第二把锁失败了,不要傻等,先把手里的锁释放掉,过会儿再试。

cpp 复制代码
// 破坏保持条件
if (pthread_mutex_trylock(&mtx2) != 0) {
    pthread_mutex_unlock(&mtx1); // 申请失败,释放占有的资源,给别人机会
    // 适当延时后重试
}

5 ~> STL 容器与智能指针:它们真的安全吗?

这是面试的高频题,大家一定要记清楚。

5.1 STL 容器:性能至上,安全自负

结论非常明确:C++ 标准库(STL)中的容器(vector、list、map等)绝大部分都不是线程安全的!

理由 : C++ 的设计哲学是"不为不使用的东西付费"。如果给每个 push_back 都加锁,那单线程环境下的性能会大打折扣。

解法 : 多线程访问同一个容器,请务必自己加锁

5.2 智能指针:半成品安全

  • unique_ptr:它是独占的,不涉及共享,所以天然没有多线程干扰问题(只要你不作死传递引用)。

  • shared_ptr:它的引用计数是线程安全的。标准库内部使用了原子操作(如 CAS)来保证计数增减的原子性。

  • 警告:引用计数安全,并不代表它指向的对象安全!

    • 如果你用两个线程通过不同的 shared_ptr 对象去修改同一个底层数据,仍然需要加锁。

6 ~> 我们应该了解的一些"高级锁"概念

6.1 悲观锁 VS 乐观锁

  • 悲观锁 :总觉得会有线程来改数据,所以干活前先加锁(如 mutex)。

  • 乐观锁:觉得大家都很文明。不加锁,直接改,但在更新那一刻检查一下:我改的这段时间里,别人动过没?(通常通过版本号或 CAS 实现)。

6.2 CAS (Compare-And-Swap)

这是并发编程的基石。它是一条 CPU 指令:"如果当前值等于我预期的 A,就把它改成 B,否则报错"。它是无锁(Lock-free)编程的核心。

6.3 自旋锁 VS 阻塞锁

6.3.1 生活案例:等朋友下楼

  • 自旋锁(Spin Lock) :你在楼下等朋友,每隔 10 秒打个电话问"下来没?"。你一直处于忙碌状态。适合 等待时间极短 的场景,省去了线程上下文切换的开销。

  • 阻塞锁(Mutex) :你发现朋友还得打 5 小时游戏,于是你先回家睡觉(线程挂起),等他下楼了给你打个电话把你叫醒。适合 等待时间长 的场景。


7 ~> 总结与建议

7.1 总结

并发编程就像是在刀尖上行走。我们回顾一下核心点:

  • 线程安全 是目标,可重入是函数的高级属性。

  • 死锁是因为四个条件(互斥、请求保持、不剥夺、循环等待)凑齐了,破坏一个就能脱困。

  • STL 不安全,智能指针只保计数,剩下的得靠你。

7.2 学习建议

如果你想深入了解底层原理,艾莉丝这里推荐阅读 《Unix 环境高级编程》(APUE) 这本书中关于线程同步的章节。

多看 Linux 内核中关于 spinlockrcu 的实现,那是性能艺术的巅峰。

并发编程没有捷径,唯一的秘诀就是:多写代码,多踩坑


结尾

uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ### 艾莉丝努力练剑 C/C++ & Linux 底层探索者 | 一个正在努力练剑的技术博主 *** ** * ** *** 👀 【关注】 跟随我一起深耕技术领域,见证每一次成长。 ❤️ 【点赞】 让优质内容被更多人看见,让知识传递更有力量。 ⭐ 【收藏】 把核心知识点存好,在需要时随时查、随时用。 💬 【评论】 分享你的经验或疑问,评论区一起交流避坑! 不要忘记给博主"一键四连"哦! "今日练剑达成!" "技术之路难免有困惑,但同行的人会让前进更有方向。" |

结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主"一键四连"哦!

往期回顾

【Linux线程】Linux系统多线程(九):线程池实现(附代码示例)

🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა

相关推荐
艾莉丝努力练剑2 小时前
【Linux网络】计算机网络入门:从背景到协议,理解网络通信基础
linux·运维·服务器·c++·学习·计算机网络
_Evan_Yao2 小时前
软件工程就是一场“抽象”游戏:从 abstract 关键字到架构设计的认知跃迁
java·后端·游戏·状态模式·软件工程
23471021272 小时前
4.21 学习笔记
软件测试·笔记·python·学习
运维老郭2 小时前
Nginx vs Envoy:高并发负载均衡实战指南(含踩坑记录)
linux·运维
小娄~~2 小时前
特殊进程-
linux·运维·服务器
没有天赋那就反复2 小时前
C++里面引用参数和实参的区别
开发语言·c++·算法
Keep Running *2 小时前
Python基础_学习笔记
笔记·python·学习
ximu_polaris2 小时前
设计模式(C++)-创造型模式-建造者模式
c++·设计模式·建造者模式
AOwhisky2 小时前
Kubernetes 学习笔记:Volume 存储卷与 ConfigMap 配置管理
linux·运维·笔记·学习·云原生·kubernetes