
🎬 个人主页 :艾莉丝努力练剑
❄专栏传送门 :《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 内核中关于 spinlock 和 rcu 的实现,那是性能艺术的巅峰。
并发编程没有捷径,唯一的秘诀就是:多写代码,多踩坑。
结尾
uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ### 艾莉丝努力练剑 C/C++ & Linux 底层探索者 | 一个正在努力练剑的技术博主 *** ** * ** *** 👀 【关注】 跟随我一起深耕技术领域,见证每一次成长。 ❤️ 【点赞】 让优质内容被更多人看见,让知识传递更有力量。 ⭐ 【收藏】 把核心知识点存好,在需要时随时查、随时用。 💬 【评论】 分享你的经验或疑问,评论区一起交流避坑! 不要忘记给博主"一键四连"哦! "今日练剑达成!"
"技术之路难免有困惑,但同行的人会让前进更有方向。" |
结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主"一键四连"哦!
往期回顾:
【Linux线程】Linux系统多线程(九):线程池实现(附代码示例)
🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა
