一文讲透“进程,线程和协程” 本文从操做系统原理出发结合代码实践讲解了如下内容:python
什么是进程,线程和协程? 它们之间的关系是什么? 为何说Python中的多线程是伪多线程? 不一样的应用场景该如何选择技术方案? ...程序员
进程-操做系统提供的抽象概念,是系统进行资源分配和调度的基本单位,是操做系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的实体。程序自己是没有生命周期的,它只是存在磁盘上的一些指令,程序一旦运行就是进程。面试
当程序须要运行时,操做系统将代码和全部静态数据记载到内存和进程的地址空间(每一个进程都拥有惟一的地址空间,见下图所示)中,经过建立和初始化栈(局部变量,函数参数和返回地址)、分配堆内存以及与IO相关的任务,当前期准备工做完成,启动程序,OS将CPU的控制权转移到新建立的进程,进程开始运行。算法
操做系统对进程的控制和管理经过PCB(Processing Control Block),PCB一般是系统内存占用区中的一个连续存区,它存放着操做系统用于描述进程状况及控制进程运行所需的所有信息(包括:进程标识号,进程状态,进程优先级,文件系统指针以及各个寄存器的内容等),进程的PCB是系统感知进程的惟一实体。编程
一个进程至少具备5种基本状态:初始态、就绪状态、等待(阻塞)状态、执行状态、终止状态。缓存
初始状态:进程刚被建立,因为其余进程正占有CPU资源,因此得不到执行,只能处于初始状态。 就绪状态:只有处于就绪状态的通过调度才能到执行状态 等待状态:进程等待某件事件完成 执行状态:任意时刻处于执行状态的进程只能有一个(对于单核CPU来说)。 中止状态:进程结束安全
不管是在多核仍是单核系统中,一个CPU看上去都像是在并发的执行多个进程,这是经过处理器在进程间切换来实现的。 操做系统对把CPU控制权在不一样进程之间交换执行的机制称为上下文切换(context switch),即保存当前进程的上下文,恢复新进程的上下文,而后将CPU控制权转移到新进程,新进程就会从上次中止的地方开始。所以,进程是轮流使用CPU的,CPU被若干进程共享,使用某种调度算法来决定什么时候中止一个进程,并转而为另外一个进程提供服务。markdown
单核CPU双进程的状况 网络
进程根据特定的调度机制和遇到I/O中断等状况下,进行上下文切换,轮流使用CPU资源多线程
双核CPU双进程的状况
每个进程独占一个CPU核心资源,在处理I/O请求的时候,CPU处于阻塞状态
进程间数据共享
系统中的进程与其余进程共享CPU和主存资源,为了更好的管理主存,操做系统提供了一种对主存的抽象概念,即为虚拟存储器(VM)。它也是一个抽象的概念,它为每个进程提供了一个假象,即每一个进程都在独占地使用主存。
虚拟存储器主要提供了三个能力:
将主存当作是一个存储在磁盘上的高速缓存,在主存中只保存活动区域,并根据须要在磁盘和主存之间来回传送数据,经过这种方式,更高效地使用主存 为每一个进程提供一致的地址空间,从而简化存储器管理 保护每一个进程的地址空间不被其余进程破坏 因为进程拥有本身独占的虚拟地址空间,CPU经过地址翻译将虚拟地址转换成真实的物理地址,每一个进程只能访问本身的地址空间。所以,在没有其余机制(进程间通讯)的辅助下,进程之间是没法共享数据的
以python中多进程(multiprocessing)为例:
import multiprocessing
import threading
import 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)=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来源于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(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 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 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 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
复制代码
协程的执行效性能高于多线程
总结
本文从操做系统原理出发结合代码实践讲解了进程,线程和协程以及他们之间的关系。而且,总结和整理了Python实践中针对不一样的场景如何选择对应的方案,归结以下:
CPU密集型: 多进程 IO密集型: 多线程(协程维护成本较高,并且在读写文件方面效率没有显著提高) CPU密集和IO密集: 多进程+协程
※更多文章和资料|点击后方文字直达 ↓↓↓ 100GPython自学资料包 阿里云K8s实战手册 [阿里云CDN排坑指南]CDN ECS运维指南 DevOps实践手册 Hadoop大数据实战手册 Knative云原生应用开发指南 OSS 运维实战手册 云原生架构白皮书 Zabbix企业级分布式监控系统源码文档 云原生基础入门手册 10G大厂面试题戳领