台式机&硬件|什么是“进程、线程、协程”?
本文插图
作者 | 头文件
责编 | 王晓曼
来源 | 程序员小灰(ID:chengxuyuanxiaohui)
本文从操作系统原理出发结合代码实践讲解了以下内容:
- 什么是进程 , 线程和协程?
- 它们之间的关系是什么?
- 为什么说Python中的多线程是伪多线程?
- 不同的应用场景该如何选择技术方案?
- ...
当程序需要运行时 , 操作系统将代码和所有静态数据记载到内存和进程的地址空间(每个进程都拥有唯一的地址空间 , 见下图所示)中 , 通过创建和初始化栈(局部变量 , 函数参数和返回地址)、分配堆内存以及与IO相关的任务 , 当前期准备工作完成 , 启动程序 , OS将CPU的控制权转移到新创建的进程 , 进程开始运行 。
本文插图
操作系统对进程的控制和管理通过PCB(Processing Control Block) , PCB通常是系统内存占用区中的一个连续存区 , 它存放着操作系统用于描述进程情况及控制进程运行所需的全部信息(进程标识号,进程状态,进程优先级,文件系统指针以及各个寄存器的内容等) , 进程的PCB是系统感知进程的唯一实体 。
一个进程至少具有5种基本状态:初始态、执行状态、等待(阻塞)状态、就绪状态、终止状态 。
- 初始状态:进程刚被创建 , 由于其他进程正占有CPU所以得不到执行 , 只能处于初始状态 。
- 执行状态:任意时刻处于执行状态的进程只能有一个 。
- 就绪状态:只有处于就绪状态的经过调度才能到执行状态
- 等待状态:进程等待某件事件完成
- 停止状态:进程结束
操作系统对把CPU控制权在不同进程之间交换执行的机制成为上下文切换(context switch) , 即保存当前进程的上下文 , 恢复新进程的上下文 , 然后将CPU控制权转移到新进程 , 新进程就会从上次停止的地方开始 。 因此 , 进程是轮流使用CPU的 , CPU被若干进程共享 , 使用某种调度算法来决定何时停止一个进程 , 并转而为另一个进程提供服务 。
单核CPU双进程的情况
本文插图
进程直接特定的机制和遇到I/O中断的情况下 , 进行上下文切换 , 轮流使用CPU资源
双核CPU双进程的情况
本文插图
每一个进程独占一个CPU核心资源 , 在处理I/O请求的时候 , CPU处于阻塞状态
进程间数据共享 系统中的进程与其他进程共享CPU和主存资源 , 为了更好的管理主存 , 现在系统提供了一种对主存的抽象概念 , 即为虚拟存储器(VM) 。 它是一个抽象的概念 , 它为每一个进程提供了一个假象 , 即每个进程都在独占地使用主存 。
虚拟存储器主要提供了三个能力:
- 将主存看成是一个存储在磁盘上的高速缓存 , 在主存中只保存活动区域 , 并根据需要在磁盘和主存之间来回传送数据 , 通过这种方式 , 更高效地使用主存
- 以python中multiprocessing为例
import multiprocessingimport threadingimport time n = 0 def count(num): global n for i in range(100000): n += i print("Process {0}:n={1},id(n)={2}".format(num, n, id(n))) if __name__ == "__main__": start_time = time.time process = list for i in range(5): p = multiprocessing.Process(target=count, args=(i,)) # 测试多进程使用 # p = threading.Thread(target=count, args=(i,)) # 测试多线程使用 process.append(p) for p in process: p.start for p in process: p.join print("Main:n={0},id(n)={1}".format(n, id(n))) end_time = time.time print("Total time:{0}".format(end_time - start_time)) - 结果
Process 1:n=4999950000,id(n)=139854202072440Process 0:n=4999950000,id(n)=139854329146064Process 2:n=4999950000,id(n)=139854202072400Process 4:n=4999950000,id(n)=139854201618960Process 3:n=4999950000,id(n)=139854202069320Main:n=0,id(n)=9462720Total time:0.03138256072998047 变量n在进程p{0,1,2,3,4}和主进程(main)中均拥有唯一的地址空间
什么是线程 线程-也是操作系统提供的抽象概念 , 是程序执行中一个单一的顺序控制流程 , 是程序执行流的最小单元 , 是处理器调度和分派的基本单位 。 一个进程可以有一个或多个线程 , 同一进程中的多个线程将共享该进程中的全部系统资源 , 如虚拟地址空间 , 文件描述符和信号处理等等 。 但同一进程中的多个线程有各自的调用栈和线程本地存储(如下图所示) 。
本文插图
系统利用PCB来完成对进程的控制和管理 。 同样 , 系统为线程分配一个线程控制块TCB(Thread Control Block),将所有用于控制和管理线程的信息记录在线程的控制块中 , TCB中通常包括:
- 线程标志符
- 一组寄存器
- 线程运行状态
- 优先级
- 线程专有存储区
- 信号屏蔽
和进程一样 , 线程同样有五种状态:初始态、执行状态、等待(阻塞)状态、就绪状态和终止状态,线程之间的切换和进程一样也需要上下文切换 , 这里不再赘述 。
进程和线程之间有许多相似的地方 , 那它们之间到底有什么区别呢?
进程 VS 线程
- 进程是资源的分配和调度的独立单元 。 进程拥有完整的虚拟地址空间 , 当发生进程切换时 , 不同的进程拥有不同的虚拟地址空间 。 而同一进程的多个线程是可以共享同一地址空间 。
- 线程是CPU调度的基本单元 , 一个进程包含若干线程 。
- 线程比进程小 , 基本上不拥有系统资源 。 线程的创建和销毁所需要的时间比进程小很多
- 由于线程之间能够共享地址空间 , 因此 , 需要考虑同步和互斥操作
- 一个线程的意外终止会影响整个进程的正常运行 , 但是一个进程的意外终止不会影响其他的进程的运行 。 因此 , 多进程程序安全性更高 。
什么是协程
协程(Coroutine , 又称微线程)是一种比线程更加轻量级的存在 , 协程不是被操作系统内核所管理 , 而完全是由程序所控制 。 协程与线程以及进程的关系见下图所示 。
- 协程可以比作子程序 , 但执行过程中 , 子程序内部可中断 , 然后转而执行别的子程序 , 在适当的时候再返回来接着执行 。 协程之间的切换不需要涉及任何系统调用或任何阻塞调用
- 协程只在一个线程中执行 , 是子程序之间的切换 , 发生在用户态上 。 而且 , 线程的阻塞状态是由操作系统内核来完成 , 发生在内核态上 , 因此协程相比线程节省线程创建和切换的开销
- 协程中不存在同时写变量冲突 , 因此 , 也就不需要用来守卫关键区块的同步性原语 , 比如互斥锁、信号量等 , 并且不需要来自操作系统的支持 。
本文插图
下面 , 将针对在不同的应用场景中如何选择使用Python中的进程 , 线程 , 协程进行分析 。
如何选择?
在针对不同的场景对比三者的区别之前 , 首先需要介绍一下python的多线程(一直被程序员所诟病 , 认为是"假的"多线程) 。
那为什么认为Python中的多线程是“伪”多线程呢?
更换上面multiprocessing示例中 ,p=multiprocessing.Process(target=count,args=(i,))为 p=threading.Thread(target=count,args=(i,)),其他照旧 , 运行结果如下:
为了减少代码冗余和文章篇幅 , 命名和打印不规则问题请忽略
Process 0:n=5756690257,id(n)=140103573185600Process 2:n=10819616173,id(n)=140103573185600Process 1:n=11829507727,id(n)=140103573185600Process 4:n=17812587459,id(n)=140103573072912Process 3:n=14424763612,id(n)=140103573185600Main:n=17812587459,id(n)=140103573072912Total time:0.1056210994720459 - n是全局变量 , Main的打印结果与线程相等 , 证明了线程之间是数据共享
1、什么是GIL
GIL来源于Python设计之初的考虑 , 为了数据安全(由于内存管理机制中采用引用计数)所做的决定 。 某个线程想要执行 , 必须先拿到 GIL 。 因此 , 可以把 GIL 看作是“通行证”,并且在一个 Python进程中 , GIL 只有一个,拿不到通行证的线程,就不允许进入 CPU 执行 。
Cpython解释器在内存管理中采用引用计数 , 当对象的引用次数为0时 , 会将对象当作垃圾进行回收 。 设想这样一种场景:
一个进程中含有两个线程 , 分别为线程0和线程1 , 两个线程全都引用对象a 。 当两个线程同时对a发生引用(并未修改 , 不需要使用同步性原语) , 就会发生同时修改对象a的引用计数器 , 造成计数器引用少于实质性的引用 , 当进行垃圾回收时 , 造成错误异常 。 因此 , 需要一把全局锁(即为GIL)来保证对象引用计数的正确性和安全性 。
无论是单核还是多核,一个进程永远只能同时执行一个线程(拿到 GIL 的线程才能执行 , 如下图所示) , 这就是为什么在多核CPU上 , Python 的多线程效率并不高的根本原因 。
本文插图
那是不是在Python中遇到并发的需求就使用多进程就万事大吉了呢?其实不然 , 软件工程中有一句名言:没有银弹!
2、何时用? 常见的应用场景不外乎三种:
- CPU密集型:程序需要占用CPU进行大量的运算和数据处理;
- I/O密集型:程序中需要频繁的进行I/O操作;例如网络中socket数据传输和读取等;
- CPU密集+I/O密集:以上两种的结合
下面主要解释一下I/O密集型的情况 。 与I/O设备交互 , 目前最常用的解决方案就是DMA 。
3、什么是DMA DMA(Direct Memory Access)是系统中的一个特殊设备 , 它可以协调完成内存到设备间的数据传输 , 中间过程不需要CPU介入 。
以文件写入为例:
- 进程p1发出数据写入磁盘文件的请求
- CPU处理写入请求 , 通过编程告诉DMA引擎数据在内存的位置 , 要写入数据的大小以及目标设备等信息
- CPU处理其他进程p2的请求 , DMA负责将内存数据写入到设备中
- DMA完成数据传输 , 中断CPU
- CPU从p2上下文切换到p1,继续执行p1
本文插图
Python多线程的表现(I/O密集型)
- 线程Thread0首先执行 , 线程Thread1等待(GIL的存在)
- Thread0收到I/O请求 , 将请求转发给DMA,DMA执行请求
- Thread1占用CPU资源 , 继续执行
本文插图
与进程的执行模式相似 , 弥补了GIL带来的不足 , 又由于线程的开销远远小于进程的开销 , 因此 , 在IO密集型场景中 , 多线程的性能更高
实践是检验真理的唯一标准 , 下面将针对I/O密集型场景进行测试 。
测试
- 执行代码
import multiprocessingimport threadingimport time def count(num): time.sleep(1) ## 模拟IO操作 print("Process {0} End".format(num)) if __name__ == "__main__": start_time = time.time process = list for i in range(5): p = multiprocessing.Process(target=count, args=(i,)) # p = threading.Thread(target=count, args=(i,)) process.append(p) for p in process: p.start for p in process: p.join end_time = time.time print("Total time:{0}".format(end_time - start_time)) - 结果
## 多进程Process 0 EndProcess 3 EndProcess 4 EndProcess 2 EndProcess 1 EndTotal time:1.383193016052246## 多线程Process 0 EndProcess 4 EndProcess 3 EndProcess 1 EndProcess 2 EndTotal time:1.003425121307373 - 多线程的执行效性能高于多进程
以Python中asyncio应用为依赖 , 使用async/await语法进行协程的创建和使用 。
- 程序代码
import timeimport asyncio async def coroutine: await asyncio.sleep(1) ## 模拟IO操作 if __name__ == "__main__": start_time = time.time loop = asyncio.get_event_loop tasks = for i in range(5): task = loop.create_task(coroutine) tasks.append(task) loop.run_until_complete(asyncio.wait(tasks)) loop.close end_time = time.time print("total time:", end_time - start_time) - 结果
total time: 1.001854419708252 - 协程的执行效性能高于多线程
- CPU密集型:多进程
- IO密集型:多线程(协程维护成本较高,而且在读写文件方面效率没有显著提升)
- CPU密集和IO密集:多进程+协程
【台式机&硬件|什么是“进程、线程、协程”?】
推荐阅读
- 火星时尚|李沁穿褐色褶皱上衣,配半身皮裙秀"蚂蚁"腰,俏皮又有魅力
- 火星时尚|刘雯度假穿搭真好看,格子衫配渔夫帽秀美腰,"竹竿腿"真吸睛
- 硬件是一门学问|提升帧数≠画质降低!iGame游戏显卡DLSS 2.0真香
- 新机发布|全球最贵苹果手机诞生!"土豪定制版"iPhone12发布:售价达17万
- "|分手4个月,周扬青认识很多男生却没人敢追!亲密叫阿娇"老公"
- 时装设计师|击中大卫·鲍伊&乔布斯审美,日本服装设计“四大天王”魔力何在?
- 亚洲新能源汽车网|2020下半年即将上市新能源车型&车载显示盘点
- 扬扬护肤学|被称为最丑烂大街的品牌Champion!李宁被称为国货之光是营销吗?
- 三星手机|韩国人真的"爱用国货"吗?
- 街角&祝福|人心被蒙蔽了而已
