本文从操作系统原理出发结合代码实践讲解了以下内容:
站在用户的角度思考问题,与客户深入沟通,找到吴川网站设计与吴川网站推广的解决方案,凭借多年的经验,让设计与互联网技术结合,创造个性化、用户体验好的作品,建站类型包括:网站设计制作、成都网站建设、企业官网、英文网站、手机端网站、网站推广、域名注册、虚拟空间、企业邮箱。业务覆盖吴川地区。
进程-操作系统提供的抽象概念,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的实体。程序本身是没有生命周期的,它只是存在磁盘上的一些指令,程序一旦运行就是进程。
当程序需要运行时,操作系统将代码和所有静态数据记载到内存和进程的地址空间(每个进程都拥有唯一的地址空间,见下图所示)中,通过创建和初始化栈(局部变量,函数参数和返回地址)、分配堆内存以及与IO相关的任务,当前期准备工作完成,启动程序,OS将CPU的控制权转移到新创建的进程,进程开始运行。
操作系统对进程的控制和管理通过PCB(Processing Control Block),PCB通常是系统内存占用区中的一个连续存区,它存放着操作系统用于描述进程情况及控制进程运行所需的全部信息(进程标识号,进程状态,进程优先级,文件系统指针以及各个寄存器的内容等),进程的PCB是系统感知进程的唯一实体。
一个进程至少具有5种基本状态:初始态、执行状态、等待(阻塞)状态、就绪状态、终止状态
进程间的切换
无论是在多核还是单核系统中,一个CPU看上去都像是在并发的执行多个进程,这是通过处理器在进程间切换来实现的。
操作系统对把CPU控制权在不同进程之间交换执行的机制成为上下文切换(context switch),即保存当前进程的上下文,恢复新进程的上下文,然后将CPU控制权转移到新进程,新进程就会从上次停止的地方开始。因此,进程是轮流使用CPU的,CPU被若干进程共享,使用某种调度算法来决定何时停止一个进程,并转而为另一个进程提供服务。
进程直接特定的机制和遇到I/O中断的情况下,进行上下文切换,轮流使用CPU资源
每一个进程独占一个CPU核心资源,在处理I/O请求的时候,CPU处于阻塞状态
进程间数据共享
系统中的进程与其他进程共享CPU和主存资源,为了更好的管理主存,现在系统提供了一种对主存的抽象概念,即为虚拟存储器(VM)。它是一个抽象的概念,它为每一个进程提供了一个假象,即每个进程都在独占地使用主存。
虚拟存储器主要提供了三个能力:
由于进程拥有自己独占的虚拟地址空间,CPU通过地址翻译将虚拟地址转换成真实的物理地址,每个进程只能访问自己的地址空间。因此,在没有其他机制(进程间通信)的辅助下,进程之间是无法共享数据的
- 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 线程
总之,多进程程序安全性高,进程切换开销大,效率低;多线程程序维护成本高,线程切换开销小,效率高。(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
但是,为什么多线程运行时间比多进程还要长?这与我们上面所说(线程的开销<<进程的开销)的严重不相符啊。这就是轮到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密集型的情况可以对比以上multiprocessing和threading的例子,多进程的性能 > 多线程的性能。
下面主要解释一下I/O密集型的情况。与I/O设备交互,目前最常用的解决方案就是DMA。
什么是DMA
DMA(Direct Memory Access)是系统中的一个特殊设备,它可以协调完成内存到设备间的数据传输,中间过程不需要CPU介入。
以文件写入为例:
Python多线程的表现(I/O密集型)
与进程的执行模式相似,弥补了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实践中针对不同的场景如何选择对应的方案,如下:
分享题目:一文讲透“进程、线程、协程”
本文路径:http://www.shufengxianlan.com/qtweb/news43/516393.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联