一文讲透“进程,线程和协程” 本文从操作系统原理出发结合代码实践讲解了以下内容:

什么是进程,线程和协程? 它们之间的关系是什么? 为什么说Python中的多线程是伪多线程? 不同的应用场景该如何选择技术方案? ...

什么是进程

进程-操作系统提供的抽象概念,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的实体。程序本身是没有生命周期的,它只是存在磁盘上的一些指令,程序一旦运行就是进程。

当程序需要运行时,操作系统将代码和所有静态数据记载到内存和进程的地址空间(每个进程都拥有唯一的地址空间,见下图所示)中,通过创建和初始化栈(局部变量,函数参数和返回地址)、分配堆内存以及与IO相关的任务,当前期准备工作完成,启动程序,OS将CPU的控制权转移到新创建的进程,进程开始运行。

操作系统对进程的控制和管理通过PCB(Processing Control Block),PCB通常是系统内存占用区中的一个连续存区,它存放着操作系统用于描述进程情况及控制进程运行所需的全部信息(包括:进程标识号,进程状态,进程优先级,文件系统指针以及各个寄存器的内容等),进程的PCB是系统感知进程的唯一实体。

一个进程至少具有5种基本状态:初始态、就绪状态、等待(阻塞)状态、执行状态、终止状态。

初始状态:进程刚被创建,由于其他进程正占有CPU资源,所以得不到执行,只能处于初始状态。 就绪状态:只有处于就绪状态的经过调度才能到执行状态 等待状态:进程等待某件事件完成 执行状态:任意时刻处于执行状态的进程只能有一个(对于单核CPU来讲)。 停止状态:进程结束

进程间的切换

无论是在多核还是单核系统中,一个CPU看上去都像是在并发的执行多个进程,这是通过处理器在进程间切换来实现的。 操作系统对把CPU控制权在不同进程之间交换执行的机制称为上下文切换(context switch),即保存当前进程的上下文,恢复新进程的上下文,然后将CPU控制权转移到新进程,新进程就会从上次停止的地方开始。因此,进程是轮流使用CPU的,CPU被若干进程共享,使用某种调度算法来决定何时停止一个进程,并转而为另一个进程提供服务。

单核CPU双进程的情况

进程根据特定的调度机制和遇到I/O中断等情况下,进行上下文切换,轮流使用CPU资源

双核CPU双进程的情况

每一个进程独占一个CPU核心资源,在处理I/O请求的时候,CPU处于阻塞状态

进程间数据共享

系统中的进程与其他进程共享CPU和主存资源,为了更好的管理主存,操作系统提供了一种对主存的抽象概念,即为虚拟存储器(VM)。它也是一个抽象的概念,它为每一个进程提供了一个假象,即每个进程都在独占地使用主存。

虚拟存储器主要提供了三个能力: 

将主存看成是一个存储在磁盘上的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,更高效地使用主存 为每个进程提供一致的地址空间,从而简化存储器管理 保护每个进程的地址空间不被其他进程破坏 由于进程拥有自己独占的虚拟地址空间,CPU通过地址翻译将虚拟地址转换成真实的物理地址,每个进程只能访问自己的地址空间。因此,在没有其他机制(进程间通信)的辅助下,进程之间是无法共享数据的

以python中多进程(multiprocessing)为例:

import multiprocessing
import threading
import timen = 0def count(num):global nfor i in range(100000):n += iprint("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)=139854202072440
Process 0:n=4999950000,id(n)=139854329146064
Process 2:n=4999950000,id(n)=139854202072400
Process 4:n=4999950000,id(n)=139854201618960
Process 3:n=4999950000,id(n)=139854202069320
Main:n=0,id(n)=9462720
Total time:0.03138256072998047

变量n在进程p{0,1,2,3,4}和主进程(main)中均拥有唯一的地址空间

什么是线程

线程-也是操作系统提供的抽象概念,是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,同一进程中的多个线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈和线程本地存储(如下图所示)。

系统利用PCB来完成对进程的控制和管理。同样,系统为线程分配一个线程控制块TCB(Thread Control Block),将所有用于控制和管理线程的信息记录在线程的控制块中,TCB中通常包括:

线程标志符 一组寄存器 线程运行状态 优先级 线程专有存储区 信号屏蔽

和进程一样,线程同样至少具有五种状态:初始态、就绪状态、等待(阻塞)状态、执行状态和终止状态

线程之间的切换和进程一样也需要上下文切换,这里不再赘述。

进程和线程之间有许多相似的地方,那它们之间到底有什么区别呢? 进程 VS 线程

进程是资源的分配和调度的独立单元。进程拥有完整的虚拟地址空间,当发生进程切换时,不同的进程拥有不同的虚拟地址空间。而同一进程的多个线程共享同一地址空间(不同进程之间的线程无法共享) 线程是CPU调度的基本单元,一个进程包含若干线程(至少一个线程)。 线程比进程小,基本上不拥有系统资源。线程的创建和销毁所需要的时间比进程小很多 由于线程之间能够共享地址空间,因此,需要考虑同步和互斥操作 一个线程的意外终止会影响整个进程的正常运行,但是一个进程的意外终止不会影响其他的进程的运行。因此,多进程程序安全性更高。 总之,多进程程序安全性高,进程切换开销大,效率低;多线程程序维护成本高,线程切换开销小,效率高。(python的多线程是伪多线程,下文中将详细介绍)

什么是协程

协程(Coroutine,又称微线程)是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制。协程与线程以及进程的关系见下图所示。

协程可以比作子程序,但执行过程中,子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来继续执行。协程之间的切换不需要涉及任何系统调用或任何阻塞调用 协程只在一个线程中执行,是子程序之间的切换,发生在用户态上。而且,线程的阻塞状态是由操作系统内核来完成,发生在内核态上,因此协程相比线程节省了线程创建和切换的开销 协程中不存在同时写变量冲突,因此,也就不需要用来守卫关键区块的同步性原语,比如互斥锁、信号量等,并且不需要来自操作系统的支持。

协程适用于IO阻塞且需要大量并发的场景,当发生IO阻塞,由协程的调度器进行调度,通过将数据流yield掉,并且记录当前栈上的数据,阻塞完后立刻再通过线程恢复协程栈,并把阻塞的结果放到这个线程上去运行。

下面,将针对在不同的应用场景中如何选择使用Python中的进程,线程,协程进行分析。

如何选择?

在针对不同的场景对比三者的区别之前,首先需要介绍一下python的多线程(一直被程序员所诟病,认为是"假的"多线程)。

那为什么认为Python中的多线程是“伪”多线程呢?

更换上面multiprocessing示例中, p = multiprocessing.Process(target=count, args=(i,))为p = threading.Thread(target=count, args=(i,)),其他代码不变,运行结果如下:

为了减少代码冗余和文章篇幅,命名和打印不规则问题请忽略

Process 0:n=5756690257,id(n)=140103573185600
Process 2:n=10819616173,id(n)=140103573185600
Process 1:n=11829507727,id(n)=140103573185600
Process 4:n=17812587459,id(n)=140103573072912
Process 3:n=14424763612,id(n)=140103573185600
Main:n=17812587459,id(n)=140103573072912
Total time:0.1056210994720459

n是全局变量,Main的打印结果与线程相等,证明了线程之间是数据共享

但是,为什么多线程运行时间比多进程还要长?这与我们上面所说(线程的开销<<进程的开销)的事实严重不相符。这就要轮到Cpython(python的默认解释器)中GIL(Global Interpreter Lock,全局解释锁)登场了。

什么是GIL

GIL来源于Python设计之初的考虑,为了数据安全(由于内存管理机制中采用引用计数)所做的决定。某个线程想要执行,必须先拿到 GIL。因此,可以把 GIL 看作是“通行证”,并且在一个 Python进程中,GIL 只有一个,拿不到通行证的线程,就不允许进入 CPU 执行。 Cpython解释器在内存管理中采用引用计数,当对象的引用次数为0时,会将对象当作垃圾进行回收。(有关Python内存管理机制的相关内容可以参见面试必备:Python内存管理机制)设想这样一种场景:

一个进程中含有两个线程,分别为线程0和线程1,两个线程全都引用对象a。

当两个线程同时对a发生引用(并未修改,不需要使用同步性原语),就会发生同时修改对象a的引用计数器,造成引用计数少于实质性的引用,当进行垃圾回收时,造成内存异常错误。因此,需要一把全局锁(即为GIL)来保证对象引用计数的正确性和安全性。

无论是单核还是多核,一个进程永远只能同时执行一个线程(拿到 GIL 的线程才能执行,如下图所示),这就是为什么在多核CPU上,Python 的多线程性能不高的根本原因。

那是不是在Python中遇到并发的需求就使用多进程就万事大吉了?其实不然,软件工程中有一句名言:没有银弹!

何时用?

常见的应用场景不外乎三种:

CPU密集型:程序需要占用CPU进行大量的运算和数据处理; I/O密集型:程序中需要频繁的进行I/O操作;例如网络中socket数据传输和读取等; CPU密集+I/O密集:以上两种的结合 CPU密集型的情况可以对比上面Python中multiprocessing和threading的例子:多进程的性能 > 多线程的性能。

下面主要解释一下I/O密集型的情况。与I/O设备交互,操作系统最常用的解决方案就是DMA。

什么是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资源,继续执行 CPU收到DMA的中断请求,切换到Thread0继续执行

与进程的执行模式相似,弥补了GIL带来的缺陷,又由于线程的开销远远小于进程的开销,因此,在IO密集型场景中,多线程的性能更高

实践是检验真理的唯一标准,下面将针对I/O密集型场景进行测试。

测试

执行代码

import multiprocessing
import threading
import timedef 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 End
Process 3 End
Process 4 End
Process 2 End
Process 1 End
Total time:1.383193016052246
## 多线程
Process 0 End
Process 4 End
Process 3 End
Process 1 End
Process 2 End
Total time:1.003425121307373

多线程的执行效性能高于多进程 正如上面所述,针对I/O密集型的程序,协程的执行效率更高,因为它是程序自身所控制的,这样将节省线程创建和切换所带来的开销。

以Python中asyncio并发代码库为依赖,使用async/await语法进行协程的创建和使用。 程序代码

import time
import asyncioasync 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

协程的执行效性能高于多线程

总结

本文从操作系统原理出发结合代码实践讲解了进程,线程和协程以及他们之间的关系。并且,总结和整理了Python实践中针对不同的场景如何选择对应的方案,归结如下:

CPU密集型: 多进程 IO密集型: 多线程(协程维护成本较高,而且在读写文件方面效率没有显著提升) CPU密集和IO密集: 多进程+协程

※更多文章和资料|点击后方文字直达 ↓↓↓ 100GPython自学资料包 阿里云K8s实战手册 [阿里云CDN排坑指南]CDN ECS运维指南 DevOps实践手册 Hadoop大数据实战手册 Knative云原生应用开发指南 OSS 运维实战手册 云原生架构白皮书 Zabbix企业级分布式监控系统源码文档 Linux&Python自学资料包 10G面试题戳领

js打印线程id_一文讲透“进程,线程和协程”相关推荐

  1. python 多线程和协程结合_一文讲透 “进程、线程、协程”

    本文从操作系统原理出发结合代码实践讲解了以下内容: 什么是进程,线程和协程? 它们之间的关系是什么? 为什么说Python中的多线程是伪多线程? 不同的应用场景该如何选择技术方案? ... 什么是进程 ...

  2. 一文讲透 “进程、线程、协程”

    本文从操作系统原理出发结合代码实践讲解了以下内容: 什么是进程,线程和协程? 它们之间的关系是什么? 为什么说Python中的多线程是伪多线程? 不同的应用场景该如何选择技术方案? ... 什么是进程 ...

  3. 纯干货|一文讲透 “进程、线程、协程”

    本文从操作系统原理出发结合代码实践讲解了以下内容: 什么是进程,线程和协程? 它们之间的关系是什么? 为什么说Python中的多线程是伪多线程? 不同的应用场景该如何选择技术方案? ... 什么是进程 ...

  4. 【hadoop】一文讲透hdfs的delegation token

    1.概述 转载并且补充:一文讲透hdfs的delegation token 最近我也在研究这个,学习一下. 1.1 起因 我最近在做FLink kerberos认证.我在flink配置文件中配置正确的 ...

  5. 10自带sftp服务器_一文讲透FTP和SFTP的区别

    阅读本文约需要10分钟,您可以先关注我们或收藏本文,避免下次无法找到. FTP和SFTP都是文件传输协议,我们知道FTP使用的是20和21端口,SFTP使用的是22端口.另外,SFTP前面的S应该是S ...

  6. 双线macd指标参数最佳设置_一文讲透双线MACD指标及其实战运用

    原标题:一文讲透双线MACD指标及其实战运用 船长的舍得交易体系技术理论模型中,我们要用到两大指标,分别是均线系统和双线MACD指标. 很多小伙伴都喜欢用双线MACD这个指标,但是90%的人都不知道其 ...

  7. 【敏捷开发】一文讲透敏捷管理中的DoR、DoD与AC

    文章目录 一.需求侧:DoR 案例: DoR是什么? 如何建立DoR的标准? DoR样例 1.需求 2.交互 3.架构 二.研发侧:DoD DoD是什么? 如何建立DoD的标准? DoD样例 三.用户 ...

  8. 一文讲透『大神修炼心法』!35岁让自己过的越来越好!

    Cocos 的老铁,如果你这几天没有被麒麟子给卷到?那说明你还没有真正进入 Cocos 圈子里来.为什么这么说呢?看下面. 3月1号 23:57 | 2800+字 麒麟子全方位解读 Cocos Cyb ...

  9. 一文讲透植物内生菌研究怎么做 | 微生物专题

    内容导览 1. 隐秘而强大的植物内生菌 2. 难以区分的植物内生菌 3. 更好的植物内生细菌测序方法 3.1 LNA-16S测序鉴定内生细菌原理 3.2 LNA-16S测序鉴定内生细菌占比高达99% ...

最新文章

  1. VTK 无法解析的外部符号的解决办法
  2. 汇编语言---键盘KeyCode值列表
  3. git的smart Checkout跟force checkout的区别
  4. axis2 webservice入门学识(JS,Java,PHP调用实例源码)
  5. android 语音编码,android – 如何将录制的语音编码为ogg vorbis?
  6. 好的领导应该是什么脾气
  7. abp框架(aspnetboilerplate)设置前端报错显示
  8. “别了,小黄文” 微信打击低俗小说:2019年处理违规账号6.6万+
  9. html5中的FileReader对象
  10. python subprocess.Popen简明总结
  11. 命令行进入android设置,命令行编译生成APK
  12. Twisted入门教程(6)
  13. PDF虚拟打印机的功能详解和使用方法
  14. 机器翻译的概述(冰山一角)
  15. js 实现一个打点计时器
  16. 基于MATLAB的差分方程战斗模型
  17. 萤石云视频平台播放地址获取demo
  18. IOS NDDictionary使用中value遇到no summary如何判断为NULL
  19. 给我来一段Python求素数
  20. linux中如果编译二进制,在linux中把汇编或c程序交叉编译成二进制文件烧录开发板过程详解...

热门文章

  1. 浅谈缓存最终一致性的解决方案
  2. VirtualBox下安装Ubuntu Server 16.04
  3. 初学者如何选择合适的机器学习算法(附算法速查表)
  4. 数据库常考面试知识点
  5. leetcode 752. Open the Lock | 752. 打开转盘锁(BFS)
  6. 数据结构:排序趟数 / 比较次数与序列的原始状态有关的排序方法有哪些?
  7. java安全编码指南之:输入校验
  8. 深度比较Paxos和Raft
  9. MySQL(五)汇总和分组数据
  10. mysql navicat如何为表添加外键?