python并发编程之进程

一、什么是进程

  进程即正在执行的一个过程。进程是对正在运行程序的一个抽象

  进程的概念起源于操作系统,是操作系统最核心的概念,也是操作系统提供的最古老也是最重要的抽象概念之一。操作系统的其他所有内容都是围绕进程的概念展开的。即使可以利用的cpu只有一个(早期的计算机确实如此),也能保证支持(伪)并发的能力。将一个单独的cpu变成多个虚拟的cpu(多道技术:时间多路复用和空间多路复用+硬件上支持隔离),没有进程的抽象,现代计算机将不复存在。

必备的理论基础:

复制代码
#一 操作系统的作用:
    1:隐藏丑陋复杂的硬件接口,提供良好的抽象接口
    2:管理、调度进程,并且将多个进程对硬件的竞争变得有序

#二 多道技术:
    1.产生背景:针对单核,实现并发
    ps:
    现在的主机一般是多核,那么每个核都会利用多道技术
    有4个cpu,运行于cpu1的某个程序遇到io阻塞,会等到io结束再重新调度,会被调度到4个
    cpu中的任意一个,具体由操作系统调度算法决定。
    
    2.空间上的复用:如内存中同时有多道程序
    3.时间上的复用:复用一个cpu的时间片
       强调:遇到io切,占用cpu时间过长也切,核心在于切之前将进程的状态保存下来,这样
            才能保证下次切换回来时,能基于上次切走的位置继续运行
复制代码

  进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

  狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。
  广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。
第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。[3] 
进程是操作系统中最基本、重要的概念。是多道程序系统出现后,为了刻画系统内部出现的动态情况,描述系统内部各道程序的活动规律引进的一个概念,所有多道程序设计操作系统都建立在进程的基础上。
进程的概念
从理论角度看,是对正在运行的程序过程的抽象;
从实现角度看,是一种数据结构,目的在于清晰地刻画动态系统的内在规律,有效管理和调度进入计算机系统主存储器运行的程序。
操作系统引入进程的原因
动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的。
并发性:任何进程都可以同其他进程一起并发执行
独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位;
异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进
结构特征:进程由程序、数据和进程控制块三部分组成。
多个不同的进程可以包含相同的程序:一个程序在不同的数据集里就构成不同的进程,能得到不同的结果;但是执行过程中,程序不能发生改变。
进程的特征

进程与程序的区别:

程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
而进程是程序在处理机上的一次执行过程,它是一个动态的概念。
程序可以作为一种软件资料长期存在,而进程是有一定生命期的。
程序是永久的,进程是暂时的。

  注意:同一个程序执行两次,就会在操作系统中出现两个进程,所以我们可以同时运行一个软件,分别做不同的事情也不会混乱。

二、进程的调度

要想多个进程交替运行,操作系统必须对这些进程进行调度,这个调度也不是随即进行的,而是需要遵循一定的法则,由此就有了进程的调度算法。

先来先服务(FCFS)调度算法是一种最简单的调度算法,该算法既可用于作业调度,也可用于进程调度。FCFS算法比较有利于长作业(进程),而不利于短作业(进程)。由此可知,本算法适合于CPU繁忙型作业,而不利于I/O繁忙型的作业(进程)。
先来先服务调度算法
短作业(进程)优先调度算法(SJ/PF)是指对短作业或短进程优先调度的算法,该算法既可用于作业调度,也可用于进程调度。但其对长作业不利;不能保证紧迫性作业(进程)被及时处理;作业的长短只是被估算出来的。
短作业优先调度算法
时间片轮转(Round Robin,RR)法的基本思路是让每个进程在就绪队列中的等待时间与享受服务的时间成比例。在时间片轮转法中,需要将CPU的处理时间分成固定大小的时间片,例如,几十毫秒至几百毫秒。如果一个进程在被调度选中之后用完了系统规定的时间片,但又未完成要求的任务,则它自行释放自己所占有的CPU而排到就绪队列的末尾,等待下一次调度。同时,进程调度程序又去调度当前就绪队列中的第一个进程。
      显然,轮转法只能用来调度分配一些可以抢占的资源。这些可以抢占的资源可以随时被剥夺,而且可以将它们再分配给别的进程。CPU是可抢占资源的一种。但打印机等资源是不可抢占的。由于作业调度是对除了CPU之外的所有系统硬件资源的分配,其中包含有不可抢占资源,所以作业调度不使用轮转法。
在轮转法中,时间片长度的选取非常重要。首先,时间片长度的选择会直接影响到系统的开销和响应时间。如果时间片长度过短,则调度程序抢占处理机的次数增多。这将使进程上下文切换次数也大大增加,从而加重系统开销。反过来,如果时间片长度选择过长,例如,一个时间片能保证就绪队列中所需执行时间最长的进程能执行完毕,则轮转法变成了先来先服务法。时间片长度的选择是根据系统对响应时间的要求和就绪队列中所允许最大的进程数来确定的。
      在轮转法中,加入到就绪队列的进程有3种情况:
      一种是分给它的时间片用完,但进程还未完成,回到就绪队列的末尾等待下次调度去继续执行。
      另一种情况是分给该进程的时间片并未用完,只是因为请求I/O或由于进程的互斥与同步关系而被阻塞。当阻塞解除之后再回到就绪队列。
      第三种情况就是新创建进程进入就绪队列。
      如果对这些进程区别对待,给予不同的优先级和时间片从直观上看,可以进一步改善系统服务质量和效率。例如,我们可把就绪队列按照进程到达就绪队列的类型和进程被阻塞时的阻塞原因分成不同的就绪队列,每个队列按FCFS原则排列,各队列之间的进程享有不同的优先级,但同一队列内优先级相同。这样,当一个进程在执行完它的时间片之后,或从睡眠中被唤醒以及被创建之后,将进入不同的就绪队列。  
时间片轮转法
前面介绍的各种用作进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程,而且如果并未指明进程的长度,则短进程优先和基于进程长度的抢占式调度算法都将无法使用。
而多级反馈队列调度算法则不必事先知道各种进程所需的执行时间,而且还可以满足各种类型进程的需要,因而它是目前被公认的一种较好的进程调度算法。在采用多级反馈队列调度算法的系统中,调度算法的实施过程如下所述。
(1) 应设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权愈高的队列中,为每个进程所规定的执行时间片就愈小。例如,第二个队列的时间片要比第一个队列的时间片长一倍,……,第i+1个队列的时间片要比第i个队列的时间片长一倍。
(2) 当一个新进程进入内存后,首先将它放入第一队列的末尾,按FCFS原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第二队列的末尾,再同样地按FCFS原则等待调度执行;如果它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列,……,如此下去,当一个长作业(进程)从第一队列依次降到第n队列后,在第n 队列便采取按时间片轮转的方式运行。

(3) 仅当第一队列空闲时,调度程序才调度第二队列中的进程运行;仅当第1~(i-1)队列均空时,才会调度第i队列中的进程运行。如果处理机正在第i队列中为某进程服务时,又有新进程进入优先权较高的队列(第1~(i-1)中的任何一个队列),则此时新进程将抢占正在运行进程的处理机,即由调度程序把正在运行的进程放回到第i队列的末尾,把处理机分配给新到的高优先权进程。
多级反馈队列

三、进程的并行与并发

并行 : 并行是指两者同时执行,比如赛跑,两个人都在不停的往前跑;(资源够用,比如三个线程,四核的CPU )

并发 : 并发是指资源有限的情况下,两者交替轮流使用资源,比如一段路(单核CPU资源)同时只能过一个人,A走一段后,让给B,B用完继续给A ,交替使用,目的是提高效率。

区别:

并行是从宏观上,在一个时间段上可以看出是同时执行的,比如一个服务器同时处理多个session。
并发是从微观上,也就是在一个精确的时间片刻,有不同的程序在执行,这就要求必须有多个处理器。

四、同步异步阻塞非阻塞

状态介绍

  在了解其他概念之前,我们首先要了解进程的几个状态。在程序运行的过程中,由于被操作系统的调度算法控制,程序会进入几个状态:就绪,运行和阻塞。

  (1)就绪(Ready)状态

  当进程已分配到除CPU以外的所有必要的资源,只要获得处理机便可立即执行,这时的进程状态称为就绪状态。

  (2)执行/运行(Running)状态当进程已获得处理机,其程序正在处理机上执行,此时的进程状态称为执行状态。

  (3)阻塞(Blocked)状态正在执行的进程,由于等待某个事件发生而无法执行时,便放弃处理机而处于阻塞状态。引起进程阻塞的事件可有多种,例如,等待I/O完成、申请缓冲区不能满足、等待信件(信号)等。

      

同步和异步

      所谓同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。

  所谓异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列

阻塞与非阻塞

      阻塞和非阻塞这两个概念与程序(线程)等待消息通知(无所谓同步或者异步)时的状态有关。也就是说阻塞与非阻塞主要是程序(线程)等待消息通知时的状态角度来说的

同步/异步与阻塞/非阻塞

  1. 同步阻塞形式

  效率最低。拿上面的例子来说,就是你专心排队,什么别的事都不做。

  1. 异步阻塞形式

  如果在银行等待办理业务的人采用的是异步的方式去等待消息被触发(通知),也就是领了一张小纸条,假如在这段时间里他不能离开银行做其它的事情,那么很显然,这个人被阻塞在了这个等待的操作上面;

  异步操作是可以被阻塞住的,只不过它不是在处理消息时阻塞,而是在等待消息通知时被阻塞

  1. 同步非阻塞形式

  实际上是效率低下的。

  想象一下你一边打着电话一边还需要抬头看到底队伍排到你了没有,如果把打电话和观察排队的位置看成是程序的两个操作的话,这个程序需要在这两种不同的行为之间来回的切换,效率可想而知是低下的。

  1. 异步非阻塞形式

  效率更高,

  因为打电话是你(等待者)的事情,而通知你则是柜台(消息触发机制)的事情,程序没有在两种不同的操作中来回切换

  比如说,这个人突然发觉自己烟瘾犯了,需要出去抽根烟,于是他告诉大堂经理说,排到我这个号码的时候麻烦到外面通知我一下,那么他就没有被阻塞在这个等待的操作上面,自然这个就是异步+非阻塞的方式了。

  

  很多人会把同步和阻塞混淆,是因为很多时候同步操作会以阻塞的形式表现出来,同样的,很多人也会把异步和非阻塞混淆,因为异步操作一般都不会在真正的IO操作处被阻塞

五、进程的创建与结束

进程的创建

  但凡是硬件,都需要有操作系统去管理,只要有操作系统,就有进程的概念,就需要有创建进程的方式,一些操作系统只为一个应用程序设计,比如微波炉中的控制器,一旦启动微波炉,所有的进程都已经存在。

  而对于通用系统(跑很多应用程序),需要有系统运行过程中创建或撤销进程的能力,主要分为4中形式创建新的进程:

  1. 系统初始化(查看进程linux中用ps命令,windows中用任务管理器,前台进程负责与用户交互,后台运行的进程与用户无关,运行在后台并且只在需要时才唤醒的进程,称为守护进程,如电子邮件、web页面、新闻、打印)

  2. 一个进程在运行过程中开启了子进程(如nginx开启多进程,os.fork,subprocess.Popen等)

  3. 用户的交互式请求,而创建一个新进程(如用户双击暴风影音)

  4. 一个批处理作业的初始化(只在大型机的批处理系统中应用)

  无论哪一种,新进程的创建都是由一个已经存在的进程执行了一个用于创建进程的系统调用而创建的。  

1. 在UNIX中该系统调用是:fork,fork会创建一个与父进程一模一样的副本,二者有相同的存储映像、同样的环境字符串和同样的打开文件(在shell解释器进程中,执行一个命令就会创建一个子进程)

  2. 在windows中该系统调用是:CreateProcess,CreateProcess既处理进程的创建,也负责把正确的程序装入新进程。

  关于创建子进程,UNIX和windows

  1.相同的是:进程创建后,父进程和子进程有各自不同的地址空间(多道技术要求物理层面实现进程之间内存的隔离),任何一个进程的在其地址空间中的修改都不会影响到另外一个进程。

  2.不同的是:在UNIX中,子进程的初始地址空间是父进程的一个副本,提示:子进程和父进程是可以有只读的共享内存区的。但是对于windows系统来说,从一开始父进程与子进程的地址空间就是不同的。
创建进程

进程的结束

  1. 正常退出(自愿,如用户点击交互式页面的叉号,或程序执行完毕调用发起系统调用正常退出,在linux中用exit,在windows中用ExitProcess)

  2. 出错退出(自愿,python a.py中a.py不存在)

  3. 严重错误(非自愿,执行非法指令,如引用不存在的内存,1/0等,可以捕捉异常,try...except...)

  4. 被其他进程杀死(非自愿,如kill -9)

 六、在python程序中的进程操作

  运行中的程序就是一个进程。所有的进程都是通过它的父进程来创建的。因此,运行起来的python程序也是一个进程,那么我们也可以在程序中再创建进程。多个进程可以实现并发效果,也就是说,当我们的程序中存在多个进程的时候,在某些时候,就会让程序的执行速度变快。以我们之前所了解的知识,并不能实现创建进程这个功能,所以我们就需要借助python中强大的模块。

multiprocessing模块

  仔细说来,multiprocess不是一个模块而是python中一个操作、管理进程的包。multi是取自multiple多功能的意思,所以在这个包中几乎包含了和进程有关的所有子模块。由于提供的子模块非常多,大致分为四个部分:创建进程部分、进程同步部分、进程池部分、进程之间数据共享

1.创建进程部分

multiprocessing.Process模块

process模块的介绍

process模块是一个创建进程的模块,借助这个模块,就可以完成进程的创建。

import os
from multiprocessing import Process
def func(args):
    print('in func')
    print(args)
    print('子进程',os.getpid())
    print('子进程的父进程',os.getppid())
if __name__ == '__main__':
    p = Process(target=func,args=(2,))      #进程的就绪
    p.start()   #开启了一个子进程
    print('父进程',os.getpid())
    print('父进程的父进程',os.getppid())    #主进程的父进程是python解释器的进程

#参数说明:
# Process(group=None, target=None, name=None, args=(), kwargs={})   创建一个进程对象
#进程对象.start()  启动子进程
#group   参数未使用,值始终默认为None
#target  表示调用对象,即子进程要执行的任务   调用函数时,必须是函数名,因为内部调用的是内存地址
#args    表示调用对象的位置参数元组  给target中的调用对象传参,传位置参数,是一个元组形式,必须有逗号
#kwargs  表示调用对象的字典  跟args同样,传关键字传参   是一个字典形式   {'name':'ston'}
#name    为子进程的名称,可以给子进程命名
#os.getpid()    获取当前进程中的进程编号
#os.getppid()   获取当前进程中的父进程编号
单进程创建
1 p.start():启动进程,并调用该子进程中的p.run() 
2 p.run():进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法  
3 p.terminate():强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果p还保存了一个锁那么也将不会被释放,进而导致死锁
4 p.is_alive():如果p仍然运行,返回True
5 p.join([timeout]):主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)。timeout是可选的超时时间,需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程  
process类的方法介绍
1 p.daemon:默认值为False,如果设为True,代表p为后台运行的守护进程,当p的父进程终止时,p也随之终止,并且设定为True后,p不能创建自己的新进程,必须在p.start()之前设置
2 p.name:进程的名称
3 p.pid:进程的pid
4 p.exitcode:进程在运行时为None、如果为–N,表示被信号N结束(了解即可)
5 p.authkey:进程的身份验证键,默认是由os.urandom()随机生成的32字符的字符串。这个键的用途是为涉及网络连接的底层进程间通信提供安全性,这类连接只有在具有相同的身份验证键时才能成功(了解即可)
process类的属性介绍

在Windows中使用Process模块的注意事项

    在Windows操作系统中由于没有fork(linux操作系统中创建进程的机制),在创建子进程的时候会自动 import 启动它的这个文件,而在 import 的时候又执行了整个文
件。因此如果将process()直接写在文件中就会无限递归创建子进程报错。所以必须把创建子进程的部分使用if __name__ ==‘__main__’ 判断保护起来,import 的时候,
就不会递归运行了。

  注意:开启了子进程的主进程:一般情况下,主进程会在自己的代码执行完毕和自己的子进程代码执行完毕后才结束。也有特殊情况,子进程没有结束,父进程就结束了。具体看子进程与主进程之间的关系。

多个进程的同时使用:(注意,子进程的执行顺序不是根据启动顺序决定的,而是由操作系统调度)

import time
from multiprocessing import Process
def func(arg1,arg2):
    print(arg1)
    time.sleep(2)
    print(arg2)

if __name__ == '__main__':
    for i in range(10):    #开启10个进程
        p = Process(target=func,args=('*'*i,'='*i))
        p.start()
多继承的开启

开启进程的方法,还有一种是通过继承Process类来开启进程

自定义类实现开启进程必须:1.继承Process类   2.必须实现一个run方法,run方法是在子进程中执行的代码

import os
import time
from multiprocessing import Process
class MyProcess(Process):
    def __init__(self,name,age):
        super().__init__()
    def run(self):
        print(self.name)
        print(self.pid)
        print('=======')
        time.sleep(5)
        print('in run',os.getpid())
        print('*********')
if __name__ == '__main__':
    p1 = MyProcess('rain',23)
    p1.start()
    print(os.getpid())
    print('>>>>>>>>')
    p2 = MyProcess('snow',21)
    p2.start()   #对象.start()等同于内部调用了run方法
继承process类实现开启继承

join方法的使用:

#没有使用join方法
import time
from multiprocessing import Process
def func():
    print('>>>>>>>>>')
    time.sleep(5)
    print('*************')

if __name__ == '__main__':
    p = Process(target = func)
    p.start()
    print('=============')
#输出结果:先打印============ 和 >>>>>>>>> 五秒后打印*************
未使用join方法效果
#使用join方法后
import time
from multiprocessing import Process
def func():
    print('>>>>>>>>>>>>')
    time.sleep(5)
    print('*************')

if __name__ == '__main__':
    p = Process(target = func)
    p.start()
    p.join()    #进程对象.join()
    print('=============')
#输出结果:先打印>>>>>>>>>>>>,五秒后打印***********和=============
使用join方法效果

  由此可知,join方法可以感知一个子进程的结束,使得主进程与子进程之间从异步转变为同步,在一些应用场景下有重要意义。比如,在开启多个进程时,往不同文件中写入不同或相同的内容,所有文件写完后,查看所有的文件。

多进程join方法实现多个子进程全部执行完毕后在执行主进程中的代码

import time
from multiprocessing import Process
def func(arg1,arg2):
    print(arg1)
    time.sleep(2)
    print(arg2)
if __name__ == '__main__':
    p_lis = []
    for i in range(10):
        p = Process(target=func,args=('*'*i,'='*i))
        p_lis.append(p)
        p.start()
    [p.join() for p in p_lis]   #之前的所有的进程都必须在这里执行完毕才能执行下面的代码
    print('++++++++')
View Code

 简单的实现文件的异步操作,同步查看

#借助os.walk
import os
from multiprocessing import Process
def write_file(filename,content):
    with open(filename,'a',encoding='utf-8') as f:
        f.write(content*'=')
if __name__ == '__main__':
    p_lis = []
    for i in range(5):
        p = Process(target=write_file,args=('info%s'%i,i))
        p.start()
        p_lis.append(p)
    [p.join() for p in p_lis]
    print([i for i in os.walk(r'目录')])
View Code

注意:不同进程之间不通过特殊的手段,是不可能共享一个数据的,这就是进程间的数据隔离问题

使用多进程实现TCP协议中socket服务端的并发功能

import socket
from multiprocessing import Process
def server(conn):
    conn.send(b'hello')
    msg = conn.recv(1024)
    print(msg)
    conn.close()
if __name__ == '__main__':
    sk = socket.socket()
    sk.bind(('127.0.0.1',12345))
    sk.listen()
    try:
        while True:
            conn,addr = sk.accept()
            p = Process(target=server,args=(conn,))
            p.start()
    finally:
        sk.close()
server端
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',12345))
msg = sk.recv(1024)
print(msg)
ret = input(">>")
sk.send(ret.encode('utf-8'))
sk.close()
client端

注意:在子进程中不能使用input()方法,子进程中的输入无法在主进程中显示,就会报错。

守护进程:会随着主进程的代码执行完毕而结束

主进程创建守护进程

  其一:守护进程会在主进程代码执行结束后就终止

  其二:守护进程内无法再开启子进程,否则抛出异常:AssertionError: daemonic processes are not allowed to have children

注意:进程之间是互相独立的,主进程代码运行结束,守护进程随即终止。

import time
from multiprocessing import Process
def func():
    while 1:
        print('运行正常')
        time.sleep(0.5)
def func1():
    time.sleep(8)
    print('执行 in func1')

if __name__ == '__main__':
    p = Process(target=func)
    p.daemon = True
    p.start()
    Process(target=func1).start()
    count = 1
    while count <5:
        print('server 执行中')
        time.sleep(1)
        count += 1
#输出结果:主进程与两个子进程同时执行,当主进程的代码执行完毕,守护进程随即结束,而子进程func1没有执行结束,
# 所以主进程没有执行结束,而此时的守护进程结束,因此,守护进程是随着主进程代码的执行结束而结束,而不是主进程结束而结束
守护进程误区

  注意:p.daemon = True 一定要在p.start()之前设置,设置p为守护进程,禁止p创建子进程,并且父进程代码执行结束,p即终止运行

import time
from multiprocessing import Process
def func():
    time.sleep(8)
    print('in func')
if __name__ == '__main__':
    p = Process(target=func)
    p.start()
    print(p.is_alive())  #判断一个子进程是否在执行
    time.sleep(5)
    print(p.is_alive())
    p.terminate()  #结束一个子进程
    print(p.is_alive())  #True  #中止子进程后,子进程不会立即结束,因为操作系统接到响应到执行需要时间,结束进程后立即判断,这个时间小于操作系统处理的时间
    time.sleep(1)
    print(p.is_alive())  
is_alive 和 terminate的使用

 2.进程同步部分

1.锁——multiprocessing.Lock

   程序的异步,让多个任务可以同时在几个进程中并发处理,他们之间的运行没有顺序,一旦开启也不受我们控制。尽管并发编程让我们能更加充分的利用IO资源,但是也给我们带来了新的问题。

  当多个进程使用同一份数据资源的时候,就会引发数据安全或顺序混乱问题。

我们以模拟抢票为例,来看看数据安全的重要性。

#文件db的内容为:{"ticket":3}
#注意一定要用双引号,不然json无法识别
#并发运行,效率高,但竞争写同一文件,数据写入错乱
import time
import json
from multiprocessing import Process
def show(i):
    with open('ticket')as f:
        dic = json.load(f)
    print('%s:余票%s张'%(i,dic['ticket']))
def buy_ticket(i):
    with open('ticket')as f:
        dic = json.load(f)
    time.sleep(0.1)   #模拟读数据的网络延迟
    if dic['ticket'] > 0:
        print('%s:购票成功'%i)
        dic['ticket'] -= 1
    else:
        print('%s:余票不足'%i)
    time.sleep(0.1)   #模拟写数据的网络延迟
    with open('ticket','w')as f:
        json.dump(dic,f)
if __name__ == '__main__':
    for i in range(10):
        p = Process(target=show,args=(i,))
        p.start()
    for n in range(10):
        p = Process(target=buy_ticket,args=(n,))
        p.start()
#输出结果为:2:余票3张   1:余票3张   3:余票3张   0:余票3张    6:余票3张    4:余票3张    5:余票3张   7:余票3张
#  9:余票3张   8:余票3张    0:购票成功   1:购票成功    4:购票成功   5:购票成功   3:购票成功     2:购票成功
# 8:购票成功    9:购票成功    6:购票成功    7:购票成功

# 10个用户都查看余票为3张,但是买票的时候都买到了票。因为由于网络延迟的原因,所有的并发程序都对一份数据操作就会
#造成数据混乱,导致三张余票卖给了10个人。
多进程同时抢购余票
#数据同上
import time
import json
from multiprocessing import Process,Lock
def show(i):
    with open('ticket')as f:
        dic = json.load(f)
    print('%s:余票%s张'%(i,dic['ticket']))
def buy_ticket(i,lock):
    lock.acquire()
    with open('ticket')as f:
        dic = json.load(f)
    time.sleep(0.1)
    if dic['ticket'] > 0:
        print('%s:购票成功'%i)
        dic['ticket'] -= 1
    else:
        print('%s:余票不足'%i)
    time.sleep(0.1)
    with open('ticket','w')as f:
        json.dump(dic,f)
    lock.release()
if __name__ == '__main__':
    for i in range(10):
        p = Process(target=show,args=(i,))
        p.start()
    lock = Lock()
    for n in range(10):
        p = Process(target=buy_ticket,args=(n,lock))
        p.start()
#输出结果为:0:余票3张   1:余票3张   6:余票3张   2:余票3张    4:余票3张    5:余票3张    3:余票3张   7:余票3张
#  9:余票3张   8:余票3张    0:购票成功   3:购票成功    1:购票成功   2:余票不足  4:余票不足     5:余票不足
# 8:余票不足    9:余票不足    6:余票不足    7:余票不足
使用锁保证数据安全
#加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行的修改,没错,速度是慢了,但牺牲了速度却保证了数据安全。
虽然可以用文件共享数据实现进程间通信,但问题是:
1.效率低(共享数据基于文件,而文件是硬盘上的数据)
2.需要自己加锁处理

#因此我们最好找寻一种解决方案能够兼顾:1、效率高(多个进程共享一块内存的数据)2、帮我们处理好锁问题。这就是mutiprocessing模块为我们提供的基于消息的IPC通信机制:队列和管道。
队列和管道都是将数据存放于内存中
队列又是基于(管道+锁)实现的,可以让我们从复杂的锁问题中解脱出来,
我们应该尽量避免使用共享数据,尽可能使用消息传递和队列,避免处理复杂的同步和锁问题,而且在进程数目增多时,往往可以获得更好的可获展性。

2.信号量——multiprocessing.Semaphore

 

  互斥锁同时只允许一个进程更改数据,而信号量Semaphore是同时允许一定数量的进程更改数据 。

  信号量同步基于内部计数器,每调用一次acquire(),计数器减1;每调用一次release(),计数器加1.当计数器为0时,acquire()调用被阻塞。这是迪科斯彻(Dijkstra)信号量概念P()和V()的Python实现。信号量同步机制适用于访问像服务器这样的有限资源。 信号量与进程池的概念很像,但是要区分开,信号量涉及到加锁的概念。

比如:假设在商场中有四个迷你唱吧,每次一个唱吧只能进去一个人,最多进四个人,来第五个人就要等待,模拟实现

 

import time
import random
from multiprocessing import Process,Semaphore
def ktv(i,sema):
    sema.acquire()     #方法与锁一样
    print('%s走进KTV'%i)
    time.sleep(random.uniform(3,6))
    print('%s走出KTV'%i)
    sema.release()
if __name__ == '__main__':
    sema = Semaphore(4)  #与锁的区别
    for i in range(10):
        p = Process(target=ktv,args=(i,sema))
        p.start()
模拟唱吧

 

3.事件——multiprocessing.Event

  python进程的事件用于主进程控制其他进程的执行,事件主要提供了三个方法 set、wait、clear。 事件处理的机制:全局定义了一个“Flag”,如果“Flag”值为 False,那么当程序执行 event.wait() 方法时就会阻塞,如果“Flag”值为True,那么执行event.wait()方法时便不再阻塞。 event.clear():将“Flag”设置为False, event.set():将“Flag”设置为True。通过event.is_set()方法来判断一个事件的状态。在一个事件被创建后(event = Event()),默认为阻塞状态。

简单的模拟红绿灯

import time
import random
from multiprocessing import Process,Event
def cars(i,event):
    if not event.is_set():
        print('%s:等待通过'%i)
        event.wait()   #通过wait来实现控制车辆的同行
    print('%s通过'%i)
def traffic_light(event):
    while 1:
        if event.is_set():   #Ture时表示绿灯
            event.clear()    #改变事件状态为False
            print('\033[31m红灯亮了\033[0m')   #切换状态后提示
        else:
            event.set()
            print('\033[32m绿灯亮了\033[0m')
        time.sleep(5)  # 假设每五秒切换一下

if __name__ == '__main__':
    event = Event()
    p = Process(target=traffic_light,args=(event,))
    p.start()
    for i in range(1000):  #模拟车辆
        car = Process(target=cars,args=(i,event))
        car.start()
        time.sleep(random.randint(1,3))  #每隔1-3秒来一辆车
简单红绿灯的实现

3.进程间通信(IPC(Inter-Process Communication))

1.队列

Queue([maxsize]) 
创建共享的进程队列。maxsize是队列中允许的最大项数。如果省略此参数,则无大小限制。底层队列使用管道和锁定实现。另外,还需要运行支持线程以便队列中的数据传输到底层管道中。 
Queue的实例q具有以下方法:

q.get( [ block [ ,timeout ] ] ) 
返回q中的一个项目。如果q为空,此方法将阻塞,直到队列中有项目可用为止。block用于控制阻塞行为,默认为True. 如果设置为False,将引发Queue.Empty异常(定义在Queue模块中)。timeout是可选超时时间,用在阻塞模式中。如果在制定的时间间隔内没有项目变为可用,将引发Queue.Empty异常。

q.get_nowait( ) 
同q.get(False)方法。

q.put(item [, block [,timeout ] ] ) 
将item放入队列。如果队列已满,此方法将阻塞至有空间可用为止。block控制阻塞行为,默认为True。如果设置为False,将引发Queue.Empty异常(定义在Queue库模块中)。timeout指定在阻塞模式中等待可用空间的时间长短。超时后将引发Queue.Full异常。

q.qsize() 
返回队列中目前项目的正确数量。此函数的结果并不可靠,因为在返回结果和在稍后程序中使用结果之间,队列中可能添加或删除了项目。在某些系统上,此方法可能引发NotImplementedError异常。


q.empty() 
如果调用此方法时 q为空,返回True。如果其他进程或线程正在往队列中添加项目,结果是不可靠的。也就是说,在返回和使用结果之间,队列中可能已经加入新的项目。

q.full() 
如果q已满,返回为True. 由于线程的存在,结果也可能是不可靠的(参考q.empty()方法)。。
q.close() 
关闭队列,防止队列中加入更多数据。调用此方法时,后台线程将继续写入那些已入队列但尚未写入的数据,但将在此方法完成时马上关闭。如果q被垃圾收集,将自动调用此方法。关闭队列不会在队列使用者中生成任何类型的数据结束信号或异常。例如,如果某个使用者正被阻塞在get()操作上,关闭生产者中的队列不会导致get()方法返回错误。

q.cancel_join_thread() 
不会再进程退出时自动连接后台线程。这可以防止join_thread()方法阻塞。

q.join_thread() 
连接队列的后台线程。此方法用于在调用q.close()方法后,等待所有队列项被消耗。默认情况下,此方法由不是q的原始创建者的所有进程调用。调用q.cancel_join_thread()方法可以禁止这种行为。
队列方法介绍
from multiprocessing import Queue,Process
def put(q):
    q.put('hello')
def get(q):
    print(q.get())
if __name__ == '__main__':
    q = Queue()
    p = Process(target=put,args=(q,))
    p.start()
    p1 = Process(target=get,args=(q,))
    p1.start()
#结果输出:hello      进程之间可以通过队列自由的传递数据
简单的实现进程通信

生产者消费者模型

在队列中有一个特别重要的模型——生产者消费者模型

在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。

为什么要使用生产者和消费者模式

在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。

什么是生产者消费者模式

生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

#方式一
import time
import random
from multiprocessing import Process,Queue
def producer(name,food,q):
    for i in range(10):
        time.sleep(random.randint(1,2))   #模拟生产一个food的时间
        ret = '%s生产了第%s个%s'%(name,i,food)
        print(ret)
        q.put(ret)  #将生产好的food放进一个容器里

def consumer(name,q):
    while 1:
        time.sleep(random.randint(1, 2))   #模拟消费一个food的时间
        food = q.get()   #从容器中取出
        if food is None: break   #收到结束信号则结束
        print('%s消费了%s'%(name,food))

if __name__ == '__main__':
    q = Queue()
    p = Process(target=producer,args=('rain','面包',q))
    p.start()
    p1 = Process(target=producer,args=('snow','包子',q))
    p1.start()
    c = Process(target=consumer,args=('sunny',q))
    c.start()
    c1 = Process(target=consumer,args=('wind',q))
    c1.start()
    p.join()
    p1.join()
    q.put(None) #p执行完了发送一个None表示结束,消费者收到None,表示没有food,停止消费
    q.put(None)   #同样的,有几个消费者就需要发送几个None

#方式二
import time
import random
from multiprocessing import Process, Queue
def producer(name, food, q):
    for i in range(10):
        time.sleep(random.randint(1, 2))  # 模拟生产一个food的时间
        ret = '%s生产了第%s个%s' % (name, i, food)
        print(ret)
        q.put(ret)  # 将生产好的food放进一个容器里
    q.put(None)    #生产结束发送一个结束信号

def consumer(name, q):
    while 1:
        time.sleep(random.randint(1, 2))  # 模拟消费一个food的时间
        food = q.get()  # 从容器中取出
        if food is None: break  # 收到结束信号则结束
        print('%s消费了%s' % (name, food))

if __name__ == '__main__':
    q = Queue()
    Process(target=producer, args=('rain', '面包', q)).start()
    Process(target=producer, args=('snow', '包子', q)).start()
    Process(target=consumer, args=('sunny', q)).start()
    Process(target=consumer, args=('wind', q)).start()
生产者消费者代码(low)

JoinableQueue()

  创建可连接的共享进程队列。这就像是一个Queue对象,但队列允许项目的使用者通知生产者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。

JoinableQueue的实例p除了与Queue对象相同的方法之外,还具有以下方法:

q.task_done() 
使用者使用此方法发出信号,表示q.get()返回的项目已经被处理。如果调用此方法的次数大于从队列中删除的项目数量,将引发ValueError异常。

q.join() 
生产者将使用此方法进行阻塞,直到队列中所有项目均被处理。阻塞将持续到为队列中的每个项目均调用q.task_done()方法为止。 
下面的例子说明如何建立永远运行的进程,使用和处理队列上的项目。生产者将项目放入队列,并等待它们被处理。
方法介绍
import time
import random
from multiprocessing import Process,JoinableQueue
def producer(name,food,q):
    for i in range(10):
        time.sleep(random.randint(1,3))
        ret = '%s生产了第%s个%s'%(name,i,food)
        print(ret)
        q.put(ret)
    q.join()  #感知整个队列的结束,  阻塞,直到一个队列中的所有数据全部被处理完毕
def consumer(name,q):
    while 1:
        time.sleep(random.randint(1,3))
        food = q.get()
        print('%s消费了%s'%(name,food))
        q.task_done()
if __name__ == '__main__':
    q = JoinableQueue()
    p1 = Process(target=producer,args=('rain','面包',q))
    p1.start()
    p2 = Process(target=producer,args=('sunny','包子',q))
    p2.start()
    c1 = Process(target=consumer,args=('snow',q))
    c1.daemon = True
    c1.start()
    c2 = Process(target=consumer,args=('wind',q))
    c2.daemon = True
    c2.start()
    p1.join()
    p2.join()

#  在消费者这一端:
    # 每次获取一个数据
    # 处理一个数据
    # 发送一个记号 : 标志一个数据被处理成功

# 在生产者这一端:
    # 每一次生产一个数据,
    # 且每一次生产的数据都放在队列中
    # 在队列中刻上一个记号
    # 当生产者全部生产完毕之后,
    # join信号 : 已经停止生产数据了
                # 且要等待之前被刻上的记号都被消费完
                # 当数据都被处理完时,join阻塞结束

# consumer 中把所有的任务消耗完
# producer 端 的 join感知到,停止阻塞
# 所有的producer进程结束
# 主进程中的p.join结束
# 主进程中代码结束
# 守护进程(消费者的进程)结束
JoinableQueue实现生产者消费者(进阶)

 

2.管道

 Pipe在进程之间创建一条管道,并返回元组(conn1,conn2),其中conn1、conn2表示管道两端的连接对象,强调一点:必须在产生Process对象之前产生管道

#参数介绍:
duplex:默认为True,管道是全双工的,如果将duplex设置成False,conn1只能用于接收,conn2只能用于发送。
#主要方法:
    conn1.recv():接收conn2.send(obj)发送的对象。如果没有消息可接收,recv方法会一直阻塞。如果连接的另外一端已经关闭,那么recv方法会抛出EOFError。
    conn1.send(obj):通过连接发送对象。obj是与序列化兼容的任意对象
 #其他方法:
conn1.close():关闭连接。如果conn1被垃圾回收,将自动调用此方法
conn1.fileno():返回连接使用的整数文件描述符
conn1.poll([timeout]):如果连接上的数据可用,返回True。timeout指定等待的最长时限。如果省略此参数,方法将立即返回结果。如果将timeout射成None,操作将无限期地等待数据到达。
 
conn1.recv_bytes([maxlength]):接收c.send_bytes()方法发送的一条完整的字节消息。maxlength指定要接收的最大字节数。如果进入的消息,超过了这个最大值,将引发IOError异常,并且在连接上无法进行进一步读取。如果连接的另外一端已经关闭,再也不存在任何数据,将引发EOFError异常。
conn.send_bytes(buffer [, offset [, size]]):通过连接发送字节数据缓冲区,buffer是支持缓冲区接口的任意对象,offset是缓冲区中的字节偏移量,而size是要发送字节数。结果数据以单条消息的形式发出,然后调用c.recv_bytes()函数进行接收    
 
conn1.recv_bytes_into(buffer [, offset]):接收一条完整的字节消息,并把它保存在buffer对象中,该对象支持可写入的缓冲区接口(即bytearray对象或类似的对象)。offset指定缓冲区中放置消息处的字节位移。返回值是收到的字节数。如果消息长度大于可用的缓冲区空间,将引发BufferTooShort异常。
参数和方法介绍
from multiprocessing import Process,Pipe
def func1(conn1):
    conn1.send('你好')
def func2(conn2):
    ret = conn2.recv()
    print(ret)
if __name__ == '__main__':
    conn1,conn2 = Pipe()
    Process(target=func1,args=(conn1,)).start()
    Process(target=func2,args=(conn2,)).start()
管道的简单使用
from multiprocessing import Pipe,Process
def func(conn1,conn2):
    conn1.close()
    while True:
        try:
            msg = conn2.recv()
            print(msg)
        except EOFError:  #EOFError  为没有数据可取时主动抛出的异常,当管道所有的出口关闭时,任然接收,就会抛异常
            conn2.close()
            break
if __name__ == '__main__':
    conn1,conn2 = Pipe()
    Process(target=func,args=(conn1,conn2)).start()
    conn2.close()
    for i in range(10):
        conn1.send('你好')
    conn1.close()
管道的使用

特别注意管道端点的正确管理问题:如果是生产者或消费者中都没有使用管道的某个端点,就应将它关闭。这也说明了为何在生产者中关闭了管道的输出端,在消费者中关闭管道的输入端。如果忘记执行这些步骤,程序可能在消费者中的recv()操作上阻塞。管道是由操作系统进行引用计数的,必须在所有进程中关闭管道后才能生成EOFError异常。因此,在生产者中关闭管道不会有任何效果,除非消费者也关闭了相同的管道端点。

import time
import random
from multiprocessing import Process,Pipe

def producer(con,pro,name,food):
    con.close()
    for i in range(4):
        time.sleep(random.randint(1,3))
        f = '%s生产的%s%s'%(name,i,food)
        print(f)
        pro.send(f)
    pro.close()
def consumer(con,pro,name):
    pro.close()
    while 1:
        try:
            time.sleep(random.randint(1,3))
            food = con.recv()
            print('%s消费了%s'%(name,food))
        except EOFError:
            con.close()
            break

if __name__ == '__main__':
    con,pro = Pipe()
    p1 = Process(target=producer,args=(con,pro,'','包子'))
    p1.start()
    c1 = Process(target=consumer,args=(con,pro,''))
    c1.start()
    con.close()
    pro.close()
使用Pipe实现一对一的生产者消费者模型

值得注意的是多个消费者的竞争会造成数据的不安全问题。我们可以通过加锁来控制管道的行为,避免进程之间争抢数据。

import time
import random
from multiprocessing import Process,Pipe,Lock

def producer(con,pro,name,food):
    con.close()
    for i in range(4):
        time.sleep(random.randint(1,3))
        f = '%s生产的%s%s'%(name,i,food)
        print(f)
        pro.send(f)
    pro.send(None)
    pro.send(None)
    pro.close()
def consumer(con,pro,name,lock):
    pro.close()
    while 1:
        time.sleep(random.random())
        lock.acquire()
        food = con.recv()
        lock.release()
        if food == None:
            con.close()
            break
        print('%s消费了%s' % (name, food))


if __name__ == '__main__':
    con,pro = Pipe()
    lock = Lock()
    p1 = Process(target=producer,args=(con,pro,'','包子'))
    p1.start()
    c1 = Process(target=consumer,args=(con,pro,'',lock))
    c1.start()
    c2 = Process(target=consumer,args=(con,pro,'',lock))
    c2.start()
    con.close()
    pro.close()
管道+锁 实现生产者消费者模型

4.进程之间的数据共享

  展望未来,基于消息传递的并发编程是大势所趋,即便是使用线程,推荐做法也是将程序设计为大量独立的线程集合,通过消息队列交换数据。这样极大地减少了对使用锁定和其他同步手段的需求,还可以扩展到分布式系统中。但进程间应该尽量避免通信,即便需要通信,也应该选择进程安全的工具来避免加锁带来的问题。

以后我们会尝试使用数据库来解决现在进程之间的数据共享问题。

  进程间数据是独立的,可以借助于队列或管道实现通信,二者都是基于消息传递的 。虽然进程间数据独立,但可以通过Manager实现数据共享。

 

事实上Manager的功能远不止于此

A manager object returned by Manager() controls a server process which holds Python objects and allows other processes to manipulate them using proxies.

A manager returned by Manager() will support types list, dict, Namespace, Lock, RLock, Semaphore, BoundedSemaphore, Condition, Event, Barrier, Queue, Value and Array.
Manager模块

 

from multiprocessing import Process,Manager
def func(dic):
    dic['count'] -= 1
if __name__ == '__main__':
    m = Manager()
    dic = m.dict({'count':100})
    p_lis = []
    for i in range(50):
        p = Process(target=func,args=(dic,))
        p.start()
        p_lis.append(p)
    [p.join() for p in p_lis]
    print(dic)   #执行多次,每次的结果有差异  {'count': 52}   {'count': 50}  {'count': 51}  {'count': 54}
    #多个进程同时对一个数据更改时会混乱
Manager的问题

针对上面的例子,我们可以使用加锁来保证数据的安全性

from multiprocessing import Process,Manager,Lock
def func(dic,lock):
    lock.acquire()
    dic['count'] -= 1
    lock.release()
if __name__ == '__main__':
    m = Manager()
    lock = Lock()
    dic = m.dict({'count':100})
    p_lis = []
    for i in range(50):
        p = Process(target=func,args=(dic,lock))
        p.start()
        p_lis.append(p)
    [p.join() for p in p_lis]
    print(dic)   #{'count': 50}
加锁

5.进程池和multiprocess.Pool模块

5.1进程池

为什么要有进程池?进程池的概念。

  在程序实际处理问题过程中,忙时会有成千上万的任务需要被执行,闲时可能只有零星任务。那么在成千上万个任务需要被执行的时候,我们就需要去创建成千上万个进程么?首先,创建进程需要消耗时间,销毁进程也需要消耗时间。第二即便开启了成千上万的进程,操作系统也不能让他们同时执行,这样反而会影响程序的效率。因此我们不能无限制的根据任务开启或者结束进程。

  于是就有了进程池的概念,定义一个池子,在里面放上固定数量的进程,有需求来了,就拿一个池中的进程来处理任务,等到处理完毕,进程并不关闭,而是将进程再放回进程池中继续等待任务。如果有很多任务需要执行,池中的进程数量不够,任务就要等待之前的进程执行任务完毕归来,拿到空闲进程才能继续执行。也就是说,池中进程的数量是固定的,那么同一时间最多有固定数量的进程在运行。这样不会增加操作系统的调度难度,还节省了开闭进程的时间,也一定程度上能够实现并发效果。

  在python中,进程池中的进程数量是固定的指定的n个进程(一般开的进程数是电脑CPU的数量+1),而在其他一些语言中,进程池中的进程是有一个下限的n,也有一个上限m(一般20),进程池中的进程会根据一定时间内的任务量来有效合理的增加进程(最多不超过上限),减少进程(不少于下限),来给操作系统减负。

 

备注:关于进程池和使用Process创建进程的使用:当我们开启很少的进程(比如:1个或2个)用来做一些简单的比如开启一个守护进程之类的我们使用Process来创建启动进程,除此之外,我们都用进程池去启动一个进程。

 multiprocess.Pool模块

from multiprocessing import Pool
pool = Pool()   #进程池的创建
1 process:要创建的进程数,如果省略,将默认使用cpu_count()的值   CPU的个数
2 initializer:是每个工作进程启动时要执行的可调用对象,默认为None
3 initargs:是要传给initializer的参数组
进程池参数介绍

 

 

进程池和多进程的效率对比

import time
from multiprocessing import Pool
def func(n):
    for i in range(10):
        print(n+i)
if __name__ == '__main__':
    start = time.time()
    pool = Pool(5)    #使用进程池,每次执行5个进程
    pool.map(func,range(100))
    t = time.time() - start
    print(t)   #0.17122197151184082

from multiprocessing import Process
def func(n):
    for i in range(10):
        print(n+i)
if __name__ == '__main__':
    start = time.time()
    p_lis = []
    for i in range(100):
        p = Process(target=func,args=(i,))
        p.start()   #启动100个进程
        p_lis.append(p)
    [p.join() for p in p_lis]
    t = time.time() - start
    print(t)     #2.8937172889709473

#由此可以看出,进程池的效率更高
同样的需求不同的效率

pool.map()      def map(self, func, iterable, chunksize=None)   map方法中参数为,要调用的函数名,可迭代对象 ,内置close()和join()方法

pool.apply()       def apply(self, func, args=(), kwds={})        apply方法中参数为,要调用的函数名,args=() 传参      同步的调用进程

import os
import time
from multiprocessing import Pool
def func(i):
    print(i,'start:',os.getpid())
    time.sleep(0.5)
    print(i,'end:',os.getpid())
if __name__ == '__main__':
    pool = Pool(5)
    for i in range(10):
        pool.apply(func,args=(i,))
#输出结果可以看出,apply方法是一个同步提交任务的方法
apply

pool.apply_async()      def apply_async(self, func, args=(), kwds={}, callback=None)           异步的调用进程

import os
import time
from multiprocessing import Pool
def func(i):
    print(i,'start:',os.getpid())
    time.sleep(0.5)
    print(i,'end:',os.getpid())
if __name__ == '__main__':
    pool = Pool(5)  #进程池中从无到有创建五个进程,以后一直是这五个进程在执行任务
    for i in range(10):
        pool.apply_async(func,args=(i,))   # 异步运行,根据进程池中有的进程数,每次最多5个子进程在异步执行
                                          # 需要注意的是,进程池中的五个进程不会同时开启或者同时结束
                                          # 而是执行完一个就释放一个进程,这个进程就去接收新的任务
    pool.close()     #结束进程池接收任务
    pool.join()      #感知进程池中的任务执行结束
# 异步apply_async用法:如果使用异步提交的任务,主进程需要使用join,等待进程池内任务都处理完
# 否则,主进程结束,进程池可能还没来得及执行,也就跟着一起结束
apply_async

进程池版socket并发聊天

import socket
from multiprocessing import Pool
def func(conn):
    conn.send(b'hello')
    ret = conn.recv(1024)
    print(ret)
    conn.close()
if __name__ == '__main__':
    pool = Pool(5)
    sk = socket.socket()
    sk.bind(('127.0.0.1',9000))
    sk.listen()
    while 1:
        conn,addr = sk.accept()
        pool.apply_async(func,args=(conn,))
    sk.close()
server
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',9000))

msg = sk.recv(1024).decode('utf-8')
print(msg)
content = input('>>>>>').encode('utf-8')
sk.send(content)
sk.close()
client

备注:并发开启多个客户端,服务端同一时间只有5个不同的pid,只能结束一个客户端,另外一个客户端才会进来.

5.2进程池中的返回值

备注:将子进程中的值返回,这个特性是进程池特有的。而通过Process创建的进程可以通过IPC实现将子进程中的值传回,但是无法直接return

import time
from multiprocessing import Pool
def func(n):
    time.sleep(1)
    return n**2
if __name__ == '__main__':
    pool = Pool(5)
    for i in range(10):
        p = pool.apply(func,args=(i,))
        print(p)      #apply 直接将返回值返回
apply的返回值
import time
from multiprocessing import Pool
def func(n):
    time.sleep(1)
    return n**2
if __name__ == '__main__':
    pool = Pool(5)
    for i in range(10):
        p = pool.apply_async(func,args=(i,))
        print(p.get())   #直接get()  会阻塞  等待输出结果

#解决方法就是将返回值对象添加到列表中,然后在主进程中for循环列表,再get取值
if __name__ == '__main__':
    p_lis = []
    pool = Pool(5)
    for i in range(10):
        p = pool.apply_async(func,args=(i,))
        p_lis.append(p)   #返回结果之后,将结果放入列表,归还进程,之后再执行新的任务
    for ret in p_lis:print(ret.get())    #使用get来获取apply_aync的结果,
                                        # 如果是apply,则没有get方法,因为apply是同步执行,立刻获取结果,也根本无需get
#输出结果为五个五个输出
apply_async的返回值
import time
from multiprocessing import Pool
def func(n):
    time.sleep(1)
    return n**2
if __name__ == '__main__':
    pool = Pool(5)
    ret = pool.map(func,range(10))
    print(ret)    #[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]   直接返回一个包含所有返回值的列表
    #将结果一次性的返回   也就是说要等待所有进程计算完结果后再一次性返回     因为自带join和close
map用法的返回值

  总结:由上可知,map和apply_async都提供了异步的提交返回值的方式,map更简单,但是apply_async性能更强大,可以自定制很多。map有局限性,它需要等到所有的进程执行完毕后才一次性的返回所有值,而apply_async根据你设置的进程池数量n个n个的返回结果,能够及时的进行下一步操作。值得注意的是:apply_async需要先close后join来保持多进程和主进程代码的同步性。(根据需要,如果不需要保持同步性,就可以不用)

 5.3回调函数——callback

   需要回调函数的场景:进程池中任何一个任务一旦处理完了,就立即告知主进程。主进程则调用一个函数去处理该结果,该函数即回调函数 我们可以把耗时间(阻塞)的任务放到进程池中,然后指定回调函数(主进程负责执行),这样主进程在执行回调函数时就省去了I/O的过程,直接拿到的是任务的结果。主要应用于爬虫当中。在爬虫中,最占用时间的是网络延迟,所以我们利用子进程将内容爬取下来,然后利用主进程来对字符串处理。

 

import os,time
from multiprocessing import Pool
def func1(n):
    # print('in func1',os.getpid())
    time.sleep(1)
    return n+1
def func2(m):
    print(m)
    # print('in func2',os.getpid())     #回调函数是在主进程中执行的

if __name__ == '__main__':
    # print(os.getpid())
    pool = Pool(5)
    for i in range(10):
        pool.apply_async(func1,args=(i,),callback=func2)
    pool.close()
    pool.join()
#将子进程的值作为参数传给主进程中的callback指定函数
简单的验证回调函数

 

注意:回调函数是在主进程中执行的。将子进程中函数的返回值作为参数传参给callback指定的主进程中的函数。

from urllib import request
ret = request.urlopen('http://www.baidu.com')
print(ret.read().decode('utf-8'))  #获取网址上的所有内容
urllib模块的简单用法
import requests
#requests.get(url)   #括号中加一个要爬取的网址,返回一个对象
response = requests.get('http://www.baidu.com')
print(response)    #<Response [200]>    response 响应,回应
print(response.status_code)   #200    状态码
                        ## status_code 返回404 表示访问的网页找不到    返回200  表示能正常返回    其他502   504   都是错误信息
print(response.content)   #爬取的内容   bytes类型的    b'<!DOCTYPE html>\r\n<!--STATUS OK-->....'
print(response.__dict__)    #返回一个包含状态信息的字典
requests模块的简单用法
#简单的爬取数据
import requests
from multiprocessing import Pool

def get(url):
    response = requests.get(url)
    if response.status_code == 200:
        content = response.content.decode('utf-8')
        return url,content
def dealwith(args):
    url,content = args
    print(url,len(content))    #简单的爬取内容的长度
if __name__ == '__main__':
    pool = Pool(5)
    url_list = ['http://www.baidu.com',
                'https://www.sogou.com',
                'http://www.taobao.com',
                'https://www.cnblogs.com']
    for url in url_list:
        pool.apply_async(get,args=(url,),callback=dealwith)
    pool.close()
    pool.join()
#http://www.baidu.com 2287
# https://www.sogou.com 32014
# https://www.cnblogs.com 39824
# http://www.taobao.com 124012
简单的实现爬虫
import re
from urllib.request import urlopen
from multiprocessing import Pool
def get(url,pattern):
    content = urlopen(url).read().decode('utf-8')
    return content,pattern
def dealwith(args):
    content,pattern = args
    ret = re.findall(pattern,content)
    for item in ret:
        dic = {'index':item[0].strip(),
               'title':item[1].strip(),
               'actor':item[2].strip(),
               'time':item[3].strip()}
        print(dic)
if __name__ == '__main__':
    regex = r'<dd>.*?<.*?class="board-index.*?>(\d+)</i>.*?title="(.*?)".*?class="movie-item-info".*?<p class="star">(.*?)</p>.*?<p class="releasetime">(.*?)</p>'
    #regex   正则表达式
    pattern = re.compile(regex,re.S)
    url_dic = {'http://maoyan.com/board/7': pattern}
    pool = Pool(5)
    res_l = []
    for url,pattern in url_dic.items():
        res = pool.apply_async(get,args=(url,pattern),callback=dealwith)
        res_l.append(res)
    for i in res_l:i.get()
回调函数实现一个爬虫

备注:如果在主进程中等待进程池中所有任务都执行完毕后,再统一处理结果,则无需回调函数

from multiprocessing import Pool
import time,random,os

def work(n):
    time.sleep(1)
    return n**2
if __name__ == '__main__':
    p=Pool()

    res_l=[]
    for i in range(10):
        res=p.apply_async(work,args=(i,))
        res_l.append(res)

    p.close()
    p.join() #等待进程池中所有进程执行完毕

    nums=[]
    for res in res_l:
        nums.append(res.get()) #拿到所有结果
    print(nums) #主进程拿到所有的处理结果,可以在主进程中进行统一进行处理
用close和join就不用回调函数

进程池的其他实现方式:https://docs.python.org/dev/library/concurrent.futures.html

 

posted @ 2018-07-30 17:07  一抹浅笑  阅读(300)  评论(0)    收藏  举报