本文从操做系统原理出发结合代码实践讲解了如下内容:css
什么是进程,线程和协程?python
它们之间的关系是什么?程序员
为何说Python中的多线程是伪多线程?算法
不一样的应用场景该如何选择技术方案?shell
...编程
什么是进程
进程-操做系统提供的抽象概念,是系统进行资源分配和调度的基本单位,是操做系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的实体。程序自己是没有生命周期的,它只是存在磁盘上的一些指令,程序一旦运行就是进程。swift
当程序须要运行时,操做系统将代码和全部静态数据记载到内存和进程的地址空间(每一个进程都拥有惟一的地址空间,见下图所示)中,经过建立和初始化栈(局部变量,函数参数和返回地址)、分配堆内存以及与IO相关的任务,当前期准备工做完成,启动程序,OS将CPU的控制权转移到新建立的进程,进程开始运行。缓存
操做系统对进程的控制和管理经过PCB(Processing Control Block),PCB一般是系统内存占用区中的一个连续存区,它存放着操做系统用于描述进程状况及控制进程运行所需的所有信息(进程标识号,进程状态,进程优先级,文件系统指针以及各个寄存器的内容等),进程的PCB是系统感知进程的惟一实体。安全
一个进程至少具备5种基本状态:初始态、执行状态、等待(阻塞)状态、就绪状态、终止状态微信
初始状态:进程刚被建立,因为其余进程正占有CPU因此得不到执行,只能处于初始状态。
执行状态:任意时刻处于执行状态的进程只能有一个。
就绪状态:只有处于就绪状态的通过调度才能到执行状态
等待状态:进程等待某件事件完成
中止状态:进程结束
进程间的切换
不管是在多核仍是单核系统中,一个CPU看上去都像是在并发的执行多个进程,这是经过处理器在进程间切换来实现的。
操做系统对把CPU控制权在不一样进程之间交换执行的机制成为上下文切换(context switch),即保存当前进程的上下文,恢复新进程的上下文,而后将CPU控制权转移到新进程,新进程就会从上次中止的地方开始。所以,进程是轮流使用CPU的,CPU被若干进程共享,使用某种调度算法来决定什么时候中止一个进程,并转而为另外一个进程提供服务。
单核CPU双进程的状况
进程直接特定的机制和遇到I/O中断的状况下,进行上下文切换,轮流使用CPU资源
双核CPU双进程的状况
每个进程独占一个CPU核心资源,在处理I/O请求的时候,CPU处于阻塞状态
进程间数据共享
系统中的进程与其余进程共享CPU和主存资源,为了更好的管理主存,如今系统提供了一种对主存的抽象概念,即为虚拟存储器(VM)。它是一个抽象的概念,它为每个进程提供了一个假象,即每一个进程都在独占地使用主存。
虚拟存储器主要提供了三个能力:
将主存当作是一个存储在磁盘上的高速缓存,在主存中只保存活动区域,并根据须要在磁盘和主存之间来回传送数据,经过这种方式,更高效地使用主存
为每一个进程提供了一致的地址空间,从而简化了存储器管理
保护了每一个进程的地址空间不被其余进程破坏
因为进程拥有本身独占的虚拟地址空间,CPU经过地址翻译将虚拟地址转换成真实的物理地址,每一个进程只能访问本身的地址空间。所以,在没有其余机制(进程间通讯)的辅助下,进程之间是没法共享数据的
以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调度的基本单元,一个进程包含若干线程。
线程比进程小,基本上不拥有系统资源。线程的建立和销毁所须要的时间比进程小不少
因为线程之间可以共享地址空间,所以,须要考虑同步和互斥操做
一个线程的意外终止会影像整个进程的正常运行,可是一个进程的意外终止不会影像其余的进程的运行。所以,多进程程序安全性更高。
总之,多进程程序安全性高,进程切换开销大,效率低;多线程程序维护成本高,线程切换开销小,效率高。(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)=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的打印结果与线程相等,证实了线程之间是数据共享
可是,为何多线程运行时间比多进程还要长?这与咱们上面所说(线程的开销<<进程的开销)的严重不相符啊。这就是轮到Cpython(python默认的解释器)中GIL(Global Interpreter Lock,全局解释锁)登场了。
什么是GIL
GIL来源于Python设计之初的考虑,为了数据安全(因为内存管理机制中采用引用计数)所作的决定。某个线程想要执行,必须先拿到 GIL。所以,能够把 GIL 看做是“通行证”,而且在一个 Python进程中,GIL 只有一个,拿不到通行证的线程,就不容许进入 CPU 执行。
Cpython解释器在内存管理中采用引用计数,当对象的引用次数为0时,会将对象看成垃圾进行回收。设想这样一种场景:
一个进程中含有两个线程,分别为线程0和线程1,两个线程全都引用对象a。当两个线程同时对a发生引用(并未修改,不须要使用同步性原语),就会发生同时修改对象a的引用计数器,形成计数器引用少于实质性的引用,当进行垃圾回收时,形成错误异常。所以,须要一把全局锁(即为GIL)来保证对象引用计数的正确性和安全性。
不管是单核仍是多核,一个进程永远只能同时执行一个线程(拿到 GIL 的线程才能执行,以下图所示),这就是为何在多核CPU上,Python 的多线程效率并不高的根本缘由。
那是否是在Python中遇到并发的需求就使用多进程就万事大吉了呢?其实否则,软件工程中有一句名言:没有银弹!
什么时候用?
常见的应用场景不外乎三种:
CPU密集型:程序须要占用CPU进行大量的运算和数据处理;
I/O密集型:程序中须要频繁的进行I/O操做;例如网络中socket数据传输和读取等;
CPU密集+I/O密集:以上两种的结合
CPU密集型的状况能够对比以上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 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
多线程的执行效性能高于多进程
是否是认为这就结束了?远尚未呢。针对I/O密集型的程序,协程的执行效率更高,由于它是程序自身所控制的,这样将节省线程建立和切换所带来的开销。
以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
协程的执行效性能高于多线程
总结
本文从操做系统原理出发结合代码实践讲解了进程,线程和协程以及他们之间的关系。而且,总结和整理了Python实践中针对不一样的场景如何选择对应的方案,以下:
CPU密集型:多进程
IO密集型:多线程(协程维护成本较高,并且在读写文件方面效率没有显著提高)
CPU密集和IO密集:多进程+协程
—————END—————
喜欢本文的朋友,欢迎关注公众号 程序员小灰,收看更多精彩内容
![]()
点个[在看],是对小灰最大的支持!
本文分享自微信公众号 - 程序员小灰(chengxuyuanxiaohui)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。