Cpython解释器下实现并发编程
一、 背景知识
进程(抽象概念,起源于操作系统):
一个正在执行的过程/一个正在运行的程序
操作系统:
一个协调、管理控制计算机硬件资源和软件资源的控制程序
操作系统的作用:
隐藏丑陋复杂的硬件接口,提供良好的抽象接口(用户态);管理、调度进程,并且将多个进程对硬件的竞争变得有序(内核态)
多道技术(第三代计算机;针对单核而言):
多道指的是多个程序,多道技术的实现是为了解决多个程序竞争或者说共享同一个资源(比如cpu)的有序调度问题,解决方式即多路复用,多路复用分为时间上的复用和空间上的复用。
空间上的复用:
将内存分为几部分,每个部分放入一个程序,这样,同一时间内存中就有了多道程序。
时间上的复用(复用一个CPU的时间片):
当一个程序在等待I/O时,另一个程序可以使用cpu,如果内存中可以同时存放足够多的作业,则cpu的利用率可以接近100%
操作系统采用了多道技术,可以控制进程的切换,或者说进程之间去争抢cpu的执行权限。这种切换不仅会在一个进程遇到io时进行,一个进程占用cpu时间过长也会切换,或者说被操作系统夺走cpu的执行权限
分时操作系统:多个联机终端+多道技术 (第三代计算机广泛采用了必须的保护硬件(程序之间内存彼此隔离)之后,分时系统才开始流行)
二、并发编程之多进程
程序:
一堆代码
进程:
正在进行的一个过程(任务)。而负责执行任务则是cpu。需知进程的调度、分配给哪个cpu运行,由操作系统说了算。
同一个程序执行两次,那也是两个进程,一个进程一个PID
并行:
同时运行,只有具备多个cpu才能实现并行;并行属于并发
并发:
是伪并行,即看起来是同时运行; 切换+状态保存
串行:
直到一个任务完完全全结束才开始进行下一个任务
同步:
发出一个功能调用时,在没有得到结果前,该调用就不会返回。(绝大多数函数)
异步:
当一个异步功能调用发出后,调用者不能立刻得到结果。当该异步功能完成后,通过状态(效率低)、通知或回调(效率高)来通知调用者。
同步与异步针对的是函数/任务的调用方式
阻塞:
遇到IO就发生阻塞,程序一旦遇到阻塞操作就会停在原地,并且立刻释放CPU资源
非阻塞(就绪态或运行态)
:没有遇到IO操作,或者通过某种手段让程序即便是遇到IO操作也不会停在原地,执行其他操作,力求尽可能多的占有CPU
阻塞与非阻塞指的是程序的两种运行状态
僵尸进程(有害:占用pid):
子代先于父代终结,其部分信息(pid等)没有从系统中删除,需要父代回收。join中含有回收子代信息的功能。
孤儿进程(无害):
父代先于子代终结,子代终结后的部分信息由init代收。
守护进程:
守护进程会在主进程代码执行结束后就终止并回收;守护进程内无法再开启子进程,否则抛出异常
互斥锁(进程、线程):
将并发变成串行,从而保证有序(在多个程序共享一个资源时,为保证有序不乱,需将并发变成串行),牺牲了效率而保证了数据安全。
互斥锁和join的区别:
join是按照人为指定的顺序执行,而互斥锁是所以进程平等地竞争,谁先抢到谁执行;互斥锁可以让一部分代码(修改共享数据的代码)串行,而join只能将代码整体串行
IPC通信机制(队列,管道):
进程之间通信必须找到一种介质,该介质必须满足,是所有进程共享的,必须是内存空间,附加:帮我们自动处理好锁的问题
from multiprocessing import Manager:
共享,内存,自己解决锁的问题
IPC中的队列(Queue:管道+锁)
共享,内存,自动处理锁的问题(最常用)
IPC中的管道(Pipe)
,共享,内存,需自己解决锁的问题
强调:
队列用来存成进程之间沟通的消息,数据量不应该过大;maxsize的值超过的内存限制就变得毫无意义
生产者消费者模型:
该模型中包含两类重要的角色:生产者和消费者
生产者:
将负责造数据的任务比喻为生产者
消费者:
接收生产者造出的数据来做进一步的处理,该类人物被比喻成消费者
实现生产者消费者模型三要素:生产者、消费者、队列
生产者消费者模型是解决问题的思路不是技术。可以用进程和队列来实现,也可以用其他的来实现。
生产者消费者模型应用场景:程序中出现明显的两类任何,一类任务是负责生产,另外一类任务是负责处理生产的数据的
生产者消费者模型作用:实现了生产者与消费者解耦和;平衡了生产者的生产力与消费者的处理数据的能力
三、 并发编程之多线程
线程:
指的是一条流水线的工作过程;一个进程内自带一个线程,线程是执行单位
进程不是执行单位,是资源单位
进程vs线程
同一进程内的线程们共享该进程内资源,不同进程内的线程资源肯定是隔离的
创建线程的开销比创建进程要小的多
线程中没有父子关系。相较于子线程、主线程特殊之处在于其代变了主进程的生命周期。
主进程等待子进程结束然后结束,是为子进程回收资源。主线程等待子线程结束然后结束,是等待这个进程的代码(其他非守护线程)执行完毕。
主进程:
执行完代码就结束。
主线程:
所有子线程结束才结束。
一个进程中的子线程pid(process id)相同
守护线程:
守护线程会在主进程代码执行结束(所有非守护子线程代码执行结束)后就终止并回收
对于守护进程\线程,只要进程内没有可执行的代码守护就结束
死锁现象:程序中有多把互斥锁,A持有B需要的锁、且需要B手中的锁,B持有A需要的锁、且需要A手中的锁,程序卡死。
递归锁(解决死锁问题):
是一把可连续acquire的锁,但只有其上的计数为0时其他线程才可对其调用。
信号量(semaphore,进程\线程):
5把钥匙,即可同时有5个对象进行执行
全局解释器锁(GIL):
CPython解释器的特性;
GIL本质就是一把夹在解释器身上的互斥锁(执行权限)。同一个进程内的所有线程都需要先抢到GIL锁,才能执行解释器代码
GIL优点:
保证Cpython解释器内存管理的线程安全
GIL缺点:
在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,也就说Cpython解释器的多线程无法实现并行无法利用多核优势
注意:GIL不能并行,但有可能并发,不一定为串行。因为串行是一个任务完完全全执行完毕后才进行下一个;而cpython中,一个线程在io时,被CPU释放时,会被强行取消GIL的使用权限
GIL能保护解释器级别代码(和垃圾回收机制有关)但保护不了其他共享数据(比如自己的代码)。所以在程序中对于需要保护的数据要自行加锁
计算密集型–》
使用多进程,以用上多核
IO密集型–》
使用多线程
进程池、线程池:
进程和线程都不能无限多,导入模块来限制进程和线程池重点数目,从而使计算机在一个自己可承受的范围内去并发地执行任务;进程线程池中封装了Process、Thread模块的功能
异步+回调机制(进程、线程):
使任务的返回值得到及时的处理,且使后续的处理阶段也并发执行,且使几个阶段之间解耦合;进程版:主进程作为回调的执行者;线程版:哪个子进程空闲就由那个子进程作为回调的执行者
线程queue(常用三种):
队列、堆栈、优先级队列(数字越小优先级越高)
线程event:
内部维护一个全局变量,等待函数一重置event内的值后,函数二从event.wait()后继续运行
四、并发编程之多协程
1、协程:
单线程下实现并发:协程 (为了提高效率;但不是说所有协程都会提升效率);效的协程在一定程度‘骗过’了CPU;通过自己内部协调,一遇到IO就切到自己的其他程序中,使得CPU以为这个程序一直在运行,从而使其更有可能处于就绪态或运行态,以更多的占用CPU。
2、实现并发的三种手段:
a、单线程下的并发;由程序自己控制,相对速度快
b、多线程下的并发;由操作系统控制,相对速度较慢
c、多进程下的并发;由操作系统控制,相对速度慢
3、yield:
基于yield保存状态,可实现两个任务直接来回切换,即并发的效果。但yield不会遇到阻塞自动切程序。
4、greenlet
:封装yield,遇到IO不自动切
5、gevent:
封装greenlet,不处理的话,遇到自己的IO才主动切;处理后,遇到其他IO也主动切