并发编程的理论

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也主动切

图灵python大海老师 wechat
python分享公众号
坚持原创技术分享,您的支持将鼓励我继续创作!