一篇文章吃透进程和线程,从此轻松应对所有并发场景
本文内容整理自道满《进程和线程》教程
写代码最害怕的是什么呢? 想要提高程序性能, 然而却没办法区分进程和线程之间的不同之处, 多编写了好几行代码,结果还致使服务器陷入瘫痪状态, CPU也跟着出现混乱运转的情况。
倘若告知你, 搞明白进程以及线程的底层逻辑, 随后依据任务类型挑选相应的编程模型, 便能够将服务器每一核的性能都充分挖掘出来, 你会不会相信呢?
不论是去应对面试官所出的必考分析题目, 还是自行去写爬虫程序, 又或者是处理IO逻辑, 进程以及线程都是必须要学习的基础知识。
一、多任务处理:操作系统如何“同时”运行这么多程序?
你将借助Word撰写报告之际, 同时保持着资料查询的状态, 并且后台还在凭借网盘对素材库予以下载。明明CPU所具备的就只是那几个核心, 操作系统究竟是怎样使得它们看上去仿若在同时进行运作的呢?
单核时期出现的“看似并行实则并发”情况是这样的: 操作系统采用时间片轮转方式, 给每个处于活跃状态的任务分配一小段执行时间, 这段时间通常在10到20毫秒之间, 之后CPU就在这几个任务之间进行极为快速的切换, 这种切换速度快到远远超过肉眼能够感知的程度, 于是就会让人感觉它们好像是在同步工作一样。
多核时代呈现出“真并行 + 真并发”的状况, 现代的 CPU 通常是 2 核 4 线程、4 核 8 线程甚至还包含更多的, 每一个物理核心能够独立去执行任务, 操作系统的调度器会将不同的任务分配到不一样的核心之上, 再结合时间片轮转的方式, 进而进一步地去提升并发的能力。
二、进程 vs 线程:核心模型的根本差异
对比维度
进程()
线程()
本质角色
资源分配的基本单位
CPU调度的基本单位
内存与资源
拥有独立的内存空间(代码段、堆、栈、文件描述符),相互隔离
共享所属进程的内存空间,通信效率极高
崩溃影响
一个进程崩溃,一般不影响其他进程
一个线程崩溃,很可能导致整个进程挂掉
资源开销
创建和切换开销大(分配内存、建立控制块、切换页表)
创建和切换开销极小(仅需保存PC、寄存器、栈)
通信难度
通信复杂(管道、共享内存、等)
共享内存、通信简单高效
需要关注
进程间通信成本高
数据竞争,需要锁机制来同步
想象一个进程, 它如同一间独立的房子, 有着自身一整套的生活设施, 外面的人无法进入。线程呢, 就像是这间房子里的工人, 他们共享房子里的全部资源, 然而要是工人之间争抢使用一个工具, 那就有可能引发冲突, 这种情况下就需要借助“锁”来进行协调。
三、程序员的并发选型:任务类型说了算
关于并发模型的选择, 在特定情境当中, 这取决于你的任务究竟是属于CPU密集型, 还是属于I/O密集型。一旦做出错误的选择, 那么性能不但不会提升, 反而会下降。
1. CPU密集型:视频转码、模型训练、大规模数值计算
这类任务主要消耗的是CPU资源, 其中的全局解释器锁, 也就是GIL, 是无法绕开的一道坎。GIL确保同一时刻仅有一个线程能够执行字节码, 而多线程恰恰由于锁竞争使得速度变得更为缓慢。
正确选择:多进程()
存在这样一种特征, 即每个子进程都具备独立的GIL, 凭借此GIL它能够最大限度地运用多核CPU所拥有的真正并行能力。在代码当中会运用特定方式创建4个子进程, 每个子进程会分别去调用累加, 并且该累加次数为1亿次的一项CPU计算任务, 此时四个核心能够实现全力运作。
特别是要提醒用户, 多进程启动代码, 一定要放置于if ==""的这种块儿里面, 不然的话, 就会不间断地递归着去创建子进程!
2. I/O密集型:网络爬虫、文件读写、数据库查询
在这类任务当中, 大部分的时间都处于等待网络做出响应或者磁盘进行读取的状态, 而此时CPU处于闲置状态, 可它闲着也是闲着。
正确选择:多线程()
输入输出操作会主动去释放全局解释器锁, 多个线程在这种场景状况下能够有效地去提升效率。代码之中运用点来创建线程, 与此同时去对百度、必应以及三个网站发出请求, 谁率先返回谁就率先结束, 相比串行等待三个返回而言要快出许多。
3. 具有超大规模的I/O并发情况, 其中包括Web服务器, 存在实时消息推送现象, 还有上千个HTTP请求。
存在成千上万个并发连接, 要是使用线程的话, 便会占用大量的系统资源, 而且线程切换所产生的开销也是非常大的。
正确选择:协程()
协程亦为“用户态的轻量级线程”, 其切换开销相较于线程而言更小, 于单线程之内支持超高并发。在代码当中运用async/await这一语法以及异步HTTP客户端, 仅仅只需一个线程便能够轻轻松松地处理几千个并发请求。
四、实战:一张决策流程图
┌─────────────────────┐ │ 识别任务类型 │ └──────────┬──────────┘ │ ┌─────────────────┼─────────────────┐ │ │ │ CPU密集型 I/O密集型 超大规模I/O (计算、转码) (爬虫、读写) (Web服务器) │ │ │ 多进程 多线程 协程 (multiprocessing) (threading) (asyncio)️ 核心避坑点:
当存在多个线程或者协程去访问共享资源也就是像全局变量这样的资源时, 一定要使用.Lock或者.Lock来进行保护, 不然的话将会出现竞态条件。按照固定的特定顺序去获取锁, 为设置锁设定超时的时间能够避免死锁情况的发生。在相关的情境当中, list以及dict等这类内置的数据结构并非是线程安全形态的, 在多线程的环境之下推荐使用queue.Queue。
五、总结与展望
现时代的多任务处理技术进步飞快, 早就从“单核时间片轮转”过渡演进到“多核超线程并行”, 而后又迈向“用户态协程”, 其方案愈发多样性丰富起来。对于从事开发工作的人员而言, 掌握一项核心准则便足矣:
即便受到GIL的限制, 然而它的多任务生态, 诸如种种各类等, 是极为成熟的, 完全足可以去应对绝大多数诸多场景 只要能够选对模型, 那么CPU就不会出现消极怠工的情况, 你的程序也能够切实真正地达成“多任务在同一时刻同时高效运行”!
到评论区去交流交流: 你以往项目当中有没有踩到过与多线程相关的坑? 存不存在择取模型有误从而致使性能反倒下降的经历?
学习不走弯路!
有这样一条体系化的实战路线, 它包含基础语法, 包含异步Web开发, 还包含数据采集, 以及计算机视觉, 还有NLP, 甚至大模型RAG实战。
—— 全在「道满」!