【Linux线程】Linux系统多线程(二):线程的优缺点

张开发
2026/5/19 6:14:08 15 分钟阅读
【Linux线程】Linux系统多线程(二):线程的优缺点
个人主页艾莉丝努力练剑❄专栏传送门《C语言》《数据结构与算法》《C/C干货分享学习过程记录》《Linux操作系统编程详解》《笔试/面试常见算法从基础到进阶》《Python干货分享》⭐️为天地立心为生民立命为往圣继绝学为万世开太平 艾莉丝的简介文章目录2 ~ 回顾3 ~ 线程的优缺点3.1 线程的优点3.2.1 创建一个新线程的代价要比创建一个新进程小得多3.2.2 在等待慢速I / O操作结束的同时程序可执行其他的计算任务3.2.2.1 渣男事件IO密集型举例找一个女朋友太慢了同时找多个3.2.2.2 线程不是越多越好3.2.3 重点聊线程切换的成本3.2.4 重要面试题为什么线程会快很多而且成本更低局部性原理3.2.4.1 磁盘文件预加载3.2.4.2 线程切换3.2.5 CPU内部的硬件单元MMU、CR33.2 线程的缺点3.2.1 性能损失3.2.2 健壮性降低缺乏访问控制3.2.3 代码验证3.2.3.1 源代码形成三个线程3.2.3.2 Makefile3.2.4 修改代码继续验证3.2.5 再改代码3.2.6 再再改代码3.2.7 malloc3.2.8 多线程方便的同时带来的问题3.2.9 最值得介绍的线程缺点健壮性降低3.2.10 线程异常3.2.11 线程的应用场景3.2.12 线程的缺点思维导图思维导图结尾2 ~ 回顾一个进程可以划分成多个线程。线程控制块。线程需要有优先级优先级就要有状态被切换、被调度——直接用进程的PCB来模拟对应的线程。单线程进程单执行流进程。多线程进程多执行流进程。采用轻量级进程的方式模拟线程。线程是进程内部的执行分支。线程轻量级进程CPU调度的基本单位。虚拟地址被分为3个部分前20个都是在定位物理页框位置最后12个就是低12位偏移量偏移的位置 页框起始地址就可以找到物理页框的地址。划分资源的本质就是在划分地址空间。资源划分在多线程里就是虚拟地址划分。也就是说我把代码资源划分给指定的线程本质是只要让不同的线程执行不同的函数即可因为函数在编址的时候已经做了地址空间的划分。CPU调度时可以保证线程执行不同的功能。100多万个表级太多了我把它拆成二级页表了。3 ~ 线程的优缺点3.1 线程的优点我们先瞄一眼线程的优点Linux不存在真正的线程只能提供有同等作用的载体轻量级进程LWP。当然我们在语言上通过了线程的概念就提供了线程库。我们后面谈轻量级进程的时候就把它当成线程来理解了。Windows当中存在真正的线程。我们把Linux的线程学懂了其它的平台线程都可以用了学一套线程相关的操作剩下的就都一样的——现在有AI了直接问大模型就可以啦。其实就跟我们学文件的时候一模一样底层的原理一旦学懂了就都融会贯通了。3.2.1 创建一个新线程的代价要比创建一个新进程小得多不光是创建删除也一样。创建之前申请资源的时候会浪费大量的时间创建PCB、申请虚拟地址空间、建立映射关系。创建线程更加轻量化了进程能够做的事情线程也能够做进程可以做的事线程也可以做进程的代码和线程的代码不都是CPU一行行执行的嘛但是线程占用的资源要比进程少不用重新建立大量的虚拟地址空间虚拟物理地址映射关系都不需要。创建PCB就可以了然后有一部分进程的代码给你因为线程在进程内部运行。从执行流角度能够完成某种任务的角度进程和线程一样。线程就是创建PCB然后CPU调度因为线程在进程内部运行。线程占用的资源要比进程少不用重新建立大量的虚拟地址空间也不需要虚拟地址和物理地址的映射关系。3.2.2 在等待慢速I / O操作结束的同时程序可执行其他的计算任务电脑下一个软件程序做工作的时候可以一个线程下载一个线程进行别的工作比如下一个迅雷一个线程下载一个线程可以播放——边放边播——这是多线程的优点也是多进程的优点。我们有时候电脑下软件迅雷有个能力叫边下边播以前网可没这么好。当然多进程也可以做到等待IO操作的同时执行其他的任务。不管是线程还是进程帮你完成任务无非就是两种计算密集型任务和IO密集型任务比如说我们之前写过的那些数据结构都是计算密集型任务尾插啊排序啊这种。还有一些就是属于IO密集型任务把数据从外设搬到内存内存搬到外设。我们后面会接触到IO密集型任务的。我们调的搜索引擎大模型都是计算密集型任务在我们后面工作之后数据要从A集群到B集群大部分都是IO密集型任务计算密集型的话CPU资源就利用的比较多。比如我们对大量数据进行排序可以分成两半分别在两线程进行执行依次进行运算最后多路合并——这个多进程其实也可以完成。计算密集型应用数据结构的头插尾插排序等等还有一种就是IO密集型的应用把数据从外设搬到内存内存搬到外设我们后面会接触到IO密集型任务的。调用搜索引擎、大模型都是计算密集型任务在工作后数据要从A集群到B集群大部分都是IO密集型任务计算密集型任务的话CPU资源利用比较多。对大量数据进行排序可以分成两半两个线程依次进行运算排序完再进行两路归并再排序这个多进程也可以做到。3.2.2.1 渣男事件IO密集型举例找一个女朋友太慢了同时找多个IO密集型任务很慢——IO密集型举例找一个女朋友太慢了同时找多个女朋友渣男所以后四个其实没啥特别好说的。这四个其实也是多进程的优点严格来说叫做多并发的特点。这些优点又不是只能我有。多任务处理人们更乐于使用多线程的方式来进行因为多进程更占资源多线程可以节省资源除非特别强调资源独立性用多进程。即这四个优点其实也是多进程的优点严格来说要叫多并发的优点。这些优点又不是只能我有。我们更乐于使用多线程的方式来进行因为多进程更占资源多线程节省资源。这些软件内部也是忙得很也是多线程。如果用多进程的话启动一个软件就会在任务管理器里面有多个进程就很奇怪了。节省资源企业越愿意用。多进程更占资源多线程可以节省资源除非特别强调资源独立性那就用多进程。3.2.2.2 线程不是越多越好但是线程并不是越多越好比如计算密集型两个物理CPU有一个计算任务如果只是一个线程的话另一个CPU用不上创建两个比较合适如果创建的更多就要划分更多份一个CPU执行的这个任务时间片到了肯定会涉及到切换切换本身就是一种成本所以多线程不要创建太多多了反倒会增加CPU的切换和调度的成本开销完成任务最高效率的就是单线程。计算密集型不建议创建很多建议创建数量是物理CPU数量 * 核数。IO密集型一般多创建一些问题不大比如我们下载视频同时要下载很多个所以可能同时多个线程下载其实消耗的是硬件网卡带宽资源对CPU资源的消耗还好IO密集型多创建线程无所谓但是我们也还是控制一下比较好有的也没必要那么多。3.2.3 重点聊线程切换的成本还是来看这张图进程间切换页表地址空间各方面都要切换CPU中的CR3也要切换就是切换页表。选择一个全新的进程线程和进程切换相比切换的成本更低切换线程的时候地址空间、页表都不用切换进程的切换还要保存CPU内的硬件上下文进程间切换页表地址空间各方面都要切换CPU中的CR3也要切换就是切换页表还要保存CPU的硬件上下文。切换线程时我们地址空间页表都不用切换也要把CPU的硬件上下文保存一下。我们在做切换时要区分线程切还是进程切具体怎么区分取决于时间片这个东西我们今天可以认为时间片时分配给进程的不能分配给线程如果时间片时以线程为单位分配的我就可以通过不断创建线程盗取时间片不就变相给我进程申请时间片了嘛所以是不太合理的时间片本质也是资源背后的资源叫做算力资源不止是内存进程是承担资源的基本实体单位。比如进程时间片是20ms4个线程每个线程的时间片就是5ms。当一个时间片没有消耗完我们只会进行当前进程内部的线程间切换只有当当前进程时间片消耗完才会涉及到其他的。我们切换时要区分线程切换还是进程切换——完完全全取决于时间片分配给进程的不能分配给线程——时间片要是以线程为单位分配未来程序员可以通过不断切换线程来盗取CPU内部的时间片——即变相地给进程申请时间片进程是承担资源的基本实体单位时间片背后的资源叫算力资源不仅仅是只有内存。当一个时间片没有消耗完我们只会进行当前进程内部的线程间的切换只要在当前进程的时间片消耗完之后才会涉及到其它的。那我多弄几个fork进程是不是也能无限获得时间片是的占用更多时间片谁不想让自己的软件快快地跑呢但是不能明目张胆地来用户的系统也很容易瘫痪厂商会容易竞争如果恶性竞争的话那些不会偷时间片的也就被淘汰了3.2.4 重要面试题为什么线程会快很多而且成本更低局部性原理但是上面这些这并不是线程的耗时少很多的根本原因。为什么线程会快很多成本更低——这是一个重要面试题。在我们看来快但是硬件层面上慢cache可以看成一个缓冲区我们计算机中存在一个概念叫局部性原理。在外面看来快但是硬件层面上慢CPU内部还存在cache硬件CPU内部提供的缓冲区具有临时缓冲的能力。我们在访问100行的代码下一次访问时非常的概念会访问到101行大概率就是周围的反正这就是局部性原理。当你爸在村口转悠时大概率就是在村子附近。说是大概率因为肯定也有少数情况我们不要用低概率事件否定大概率事件。不要用低概率事件否认大概率事件。3.2.4.1 磁盘文件预加载提前加载预加载正因为有了局部性原理磁盘上的文件才能提前加载到内存正是因为有了局部性原理文件才能预加载物理内存才有意义——这个说法不是很严谨但是能够理解那个意思就可以噜正是因为有了局部性原理预加载才成为有效的优化手段这叫做预加载正是因为有了局部性原理文件才能预加载内存才有意义。正是因为有了局部性原理预加载才成为了有效的优化手段——保证我们的效率提高。CPU内部存在对应的保存临时数据的缓冲区提前加载我们以后可能会直接先去cache要了没有再去内存其实我们知道有cache这回事就行了。如果是计算机科班出身的要了解cache和内存要进行数据缓存映射的方式地址和cache的行号做映射。3.2.4.2 线程切换在OS视角本质CPU正在执行某个进程或者线程的代码CPU在跑进程或者线程CPU 总是倾向于通过替换机制尽可能地让 Cache 充满当前活跃任务的数据但在物理层面上Cache 是一个动态的池子往往并存着当前进程、共享库、内核数据甚至刚刚被切走的残留数据。对应的进程当中进程间切换的时候cache永远缓存的是当前进程的数据一旦进程切换当前cache缓存的数据就全部失效了进程具有独立性其实多线程在同一个进程内部大部分资源都是共享的比如我定义一个全局变量都能看到公共函数也都可以调。进程特别强调独占性进程具有独立性——进程A和进程B代码和数据在物理内存上各自独立页表独立。多线程在同一个进程内部大部分资源数据都是共享的所以这个工作少很多是因为线程切换并不会导致当前cache中的代码和数据切换但是进程间切换这个进程是不需要上一个进程任何数据的所以我们需要再让cache热起来要重新移除缓存。这个的工作少是因为大家属于同一个进程对于其它任何一个线程都是有意义的线程切换并不会导致cache中的代码和数据失效进程间切换因为进程具有独立性上一个进程的任何数据都没有用这就是为什么线程切换更加轻量化的原因cache小的几十KB、大的几MB。虽然摩尔定律一直在推着芯片性能往前走但 Cache高速缓存 的容量增加却异常“吝啬”这主要是受限于物理成本和延迟要求。上下文切换会扰乱处理器的缓存机制。上下文切换对 Cache 的扰乱本质上是空间局部性Spatial Locality和时间局部性Temporal Locality的物理破坏。进程切换 彻底的破坏需要重构映射与数据缓存。线程切换 局部的波动保留了核心的地址映射维持了数据亲和性。这种“扰乱”也是为什么高性能服务器架构如 DPDK、Nginx 绑核竭力减少进程跨核迁移、减少上下文切换的核心原因。3.2.5 CPU内部的硬件单元MMU、CR3CPU一般在进行虚拟地址转换物理地址的过程中内部有个硬件MMU两个核心MMU、CR3。CPU得到一个虚拟地址给MMUCR3指向当前页表的起始地址这个也会给MMU出来时就是物理地址虚拟地址到物理地址的转换工作不是软件完成的而是MMU的硬件电路完成的——这个工作非常快如果让软件完成CPU的压力太大了。MMU这个硬件内部也有一个缓存MMU内部有这样一个硬件单元叫做TLB转译后备缓冲器——当成缓存来看——江湖人称“快表”。如下图所示真正的虚拟地址到物理地址的缓存没有这个每次都要查表的工作在指令级别当中把最常用的若干条映射记录存下来提高你的效率。那我进程切换这些东西也都没了下次又慢了所以线程也换比进程切换成本低的多。TLB最大的意义没了这个每次都要重复的查表的工作在指令级别当中把常用的若干条映射记录下来提高查表效率进程切换时候这些都没了但是多线程切换的还在线程切换比进程切换的成本要低得多这就是一道面试题线程切换的成本更低3.2 线程的缺点优点我们是直接论述的缺点这里我们有了学习线程优点的经验直接用代码来验证比较直观。3.2.1 性能损失性能损失这个艾莉丝上面已经说过了有可能有较大的性能损失——指的是增加了额外的同步和调度开销但是可用的资源是不变的对于计算密集型如果只有一个CPU一个核最快就是一个线程给它跑了。对于计算密集型如果只有一个CPU一个核最快的就是一个线程就把它全部跑完。3.2.2 健壮性降低缺乏访问控制健壮性降低缺乏访问控制多线程对于大部分资源都是共享的能够访问到的资源大部分都是共享的所以说缺乏访问控制。3.2.3 代码验证3.2.3.1 源代码形成三个线程主线程死循环参数带void*即可。创建两个新线程将来就有三个线程至此我们形成了三个线程。3.2.3.2 Makefile编译一下写一个Makefile-l选项还是带上Ubuntu24.04已经把线程库隐藏了-l链接这个库testthread:test.cc g-o$$^-stdc11-lpthread.PHONY:clean clean:rm-ftestthread运行系统中查看进程只能查到一个ps -aL上面的代码运行的时候还是出现了信息的错乱下面的截图没有体现这个错乱但是实际打印过程中确实出现了错乱3.2.4 修改代码继续验证运行线程2不停地改全局变量证明了所有的线程多线程共享同一个虚拟地址空间采用同一个全局变量——共享全局变量。3.2.5 再改代码运行多线程共享全局变量因为所有线程共享同一个虚拟地址空间通过页表映射也是访问的同一个物理地址。多线程对代码区全局代码其实也都是共享的。线程1和线程2甚至可以调任何一个函数但是有的没必要啊可以访问别的入口函数。我们调main函数都行但是会崩溃。在整个多线程当中多线程会对代码区也共享线程2能不能调用线程1的入口方法或者反过来线程1能不能调线程2的入口方法——能只是有没有必要代码任何一个都能够调用不要自己写main函数一调用就崩溃了当然能够调用。3.2.6 再再改代码这是不是被叫做函数重入了多个线程进入到同一个函数。出现重复打如果没出错叫可重入函数出错了就叫做不可重入函数。放大看一下不同线程进入同一个函数叫做这个函数被重入了。如果没出错叫可重入函数出错了就叫不可重入函数。向同一个显示器上打印同一个显示器就是向同一个文件写数据不一致——我们需要把显示器从共享资源变成临界资源保护起来。这里其实证明了printf不可重入出现了数据不一致。我们需要把显示器从共享资源变成临界资源保护起来才能正确。3.2.7 malloc在线程1中malloc一块堆空间线程2能不能看到原则上不能看到如果把堆区指针设置成全局呢能看到3.2.8 多线程方便的同时带来的问题在Linux中同一个进程内部多线程几乎可以共享全部的资源——多线程更加强调资源两个线程要传递内容只需要定义全局容器就可以了线程要形成进程间通信是非常容易的但是带来方便的同时也带来了并发问题等问题。结论在一个进程内部多线程几乎可以共享所有资源那想实现线程间通信是不是直接定义一个全局的结构就可以了非常容易。但是带来便利的同时也带了竞争。进程间通信困难时因为进程具有独立性代码写完鲁棒性健壮性会比较好。但是多线程的通信还是很有优势的。缺乏访问控制也可能会引起线程安全问题。进程进程间通信困难资源多独立性好代码写完之后健壮性 / 鲁棒性比较好。线程通信快多线程还是很有优势的多线程报错容易出错。3.2.9 最值得介绍的线程缺点健壮性降低最值得讲的线程的缺点其实是健壮性降低一个线程异常全进程内部的线程遭殃整个进程出问题一个线程异常整个进程内部的线程全部都遭殃整个进程出问题我们故意让一个线程出异常运行再去查三个线程全挂了Core dumped了。为什么因为线程是进程内部的执行分支线程在执行这个工作的时候线程除0就相当于进程除0都属于一个团队内部你线程就是在替进程做事的。一个线程出现异常的本质是什么是收到信号当然这之间还有中断的事——OS给你这个LWP发送的。信号是以进程为单位为载体发送的你可以理解给进程内的所有LWP都发了每个线程的pending位图都修改了所以每个线程执行处理方法这个是共享的时整个都在执行。所以整个进程都退出。3.2.10 线程异常结合上面我们举的例子这样线程异常的话题我们也说完了。3.2.11 线程的应用场景3.2.12 线程的缺点思维导图思维导图结尾uu们本文的内容到这里就全部结束了艾莉丝在这里再次感谢您的阅读艾莉丝努力练剑C/C Linux 底层探索者 | 一个正在努力练剑的技术博主【关注】跟随我一起深耕技术领域见证每一次成长。❤️【点赞】让优质内容被更多人看见让知识传递更有力量。⭐【收藏】把核心知识点存好在需要时随时查、随时用。【评论】分享你的经验或疑问评论区一起交流避坑不要忘记给博主“一键四连”哦“今日练剑达成”“技术之路难免有困惑但同行的人会让前进更有方向。”结语希望对学习Linux相关内容的uu有所帮助不要忘记给博主“一键四连”哦往期回顾【Linux线程】Linux系统多线程一线程概念博主在这里放了一只小狗大家看完了摸摸小狗放松一下吧૮₍ ˶ ˊ ᴥ ˋ˶₎ა

更多文章