Fork me on GitHub

进程并发

本文是Python通用编程系列教程,已全部更新完成,实现的目标是从零基础开始到精通Python编程语言。本教程不是对Python的内容进行泛泛而谈,而是精细化,深入化的讲解,共5个阶段,25章内容。所以,需要有耐心的学习,才能真正有所收获。虽不涉及任何框架的使用,但是会对操作系统和网络通信进行全局的讲解,甚至会对一些开源模块和服务器进行重写。学完之后,你所收获的不仅仅是精通一门Python编程语言,而且具备快速学习其他编程语言的能力,无障碍阅读所有Python源码的能力和对计算机与网络的全面认识。对于零基础的小白来说,是入门计算机领域并精通一门编程语言的绝佳教材。对于有一定Python基础的童鞋,相信这套教程会让你的水平更上一层楼。

一 进程原理

1. 进程本质

进程在执行的过程中要实现快速的切换,这是为了能够给人一种看起来好像在同时进行的样子的体验,与此同时,我们要考虑的是,这个进程切换过去了等会肯定还要在切换回来,那么再切换回来的时候,这个进程还是原来的样子吗?只有当进程还保持原来的样子这样的切换才算是有意义的,否则在切换之前CPU的工作岂不是白白浪费了吗?

所以,并发的本质其实就是:切换+保存状态

那么CPU除了在遇到IO操作和长时间计算操作被操作系统强行拿走权限之外,还有没有可能会切换进程呢?他为什么要切换进程啊?
答案是有的,因为有可能会有优先级更高的进程夺走当前进程的CPU执行权限。
接下来我们延续上一章的例子来说明:
山不转水转,冤家路窄,我和刚才被我招呼的那一队的兄弟同时进入决赛圈了,现在决赛圈只剩五个人, 我要真正开始1V4的表演了。关键时刻,我是我们全村人唯一的希望,我绝对不能死啊。我躲在一个反斜坡下,看到一个露头的小兄弟,率先发起攻击,打算先干倒一个再说,在我一顿扫射之下(我这一顿扫射就是在执行一个进程),他几乎就快要倒下了,这时,他的队友们看到了,直接3把M416扫射我,我顾不上打那个残血的兄弟,直接退到反斜坡后面(我后退这个进程就比我打敌人优先级要高,所以我要先执行),我推到后面之后,有了安全的保障,刚才被我打残血的兄弟一定在吃药(我在切换状态之前,作为CPU对他的作业状态就是把它打残血,这个状态得以保存),其他的小伙伴肯定在掩护,这时我掏出我包里的8个炸弹,给他们来一个人造轰炸区,哈哈哈。。。又吃鸡啦!
需要注意的是:一个程序运行多次,是多个进程
程序就是硬盘上的一堆文件,他是死的,但是进程是活的,一个qq程序,我登陆一个账号是起了一个进程,我在登陆另外一个账号又是起了另外一个进程。 运行程序其实就是把程序的文件从硬盘读入到内存,由CPU来执行,那么这个过程自然是可以做多次,CPU中也就会产生多个进程。
既然能够产生多个进程,那么操作系统就应该有一种方式来标识每一个进程,你怎么证明你就是你呢?

2. 验证以上说法

在验证之前先说几个命令
对于Linux或者MacOS系统的用户,执行 ps aux可以看到你的操作系统之上所有的进程,Windows系统可以执行tasklist命令,其中PID就是进程编号,如下图所以,实例图片以MacOS系统为参考

那么如何查看我们自己写的Python程序的进程呢?其实我们写的Python程序最后要运行必须依赖于Python解释器,这个解释器就是龟叔用C语言写好的,他其实就是一个软件,所以我们在运行我们自己写的Python代码的时候也就是在运行这个软件。
对于Linux或者MacOS系统的用户,执行 ps aux|grep python 命令,对于Windows系统的用户可是执行 tasklist |findstr python 可以过滤操作系统上的进程,只保留带有python关键字(区分大小写)的进程,在未执行Python代码之前,结果如下图所示:

你可以看到与 python关键字相关的进程只有我的过滤命令这一个进程,当我们把以下代码执行三次之后呢?

结果会有三个不同进程PID,这就说明:一个进程可以运行多次,而且结果是多个不同的进程

其实,我们在讲解网络编程的内容时,我们是把一个客户端复制了多次,以此来达到多个客户端的效果,其实我们并不需要这样,只要把一个客户端运行多次就可以了。
那当我们起了一个进程之后怎么干掉这个进程呢?我们刚才代码的sleep时间修改成1000秒,中途用命令干掉它,看看他在我们的IDE上面会不会终止。
对于Linux和MacOS系统的用户可以使用 kill -9 PID号,对于Windows 系统的用户可以使用taskkill /F /PID PID号来杀死一个进程,演示过程如下:
Python代码进程开始了

先过滤进程,找到进程编号为22189

使用命令杀死这个进程

程序自动终止,结果如下

我们通过命令行再次查看这个进程肯定也是没有的

除了这个之外,我们还有一种方式验证,以下代码运行多次,每次打印出来的结果都是不一样的数字

除了pid之外,还有一个ppid(parent pid)的用法,把以下代码运行三次,你会发现每次ppid都是一样的。

很明显,他爹也是一个进程,那么他爹指的是谁?接下来我们就来推演一下,我们还是用之前的命令过滤PID是19532的进程,这样就能看到他爹是哪个进程了。

为什么它的父进程是pycharm呢?
因为我们在运行自己写的程序的时候其实就是在运行Python解释器,那么谁来调用这个Python解释器谁就是谁就是这个进程他爹,调用Python解释器的自然是Pycharm。
现在我们在换一种方式,在终端调用解释器,启动我们自己写的python程序

我们Pycharm一直在运行着,所以这个进程不会发生变化,但是现在的父进程明显不是19532,因为我们并没有用Pycharm调用Python解释器,而是用终端调用的Python解释器,那么怎么验证它的父进程21108这个进程是终端的进程呢?
我们可以过滤21108这个进程,可以看到是-bash这个进程

其实 -bash 这个进程也就是我们的终端的进程的名称,Windows系统的用户这个进程的名字是 cmd,所以,如果我们提前知道了进程名字也可以以它的名字来过滤,找到这个进程的PID是21108。
Linux和MacOS系统是 ps aux|grep bash,Windows系统是 tasklist |findstr cmd,结果如下

如果我们在第二终端程序终止掉21108这个进程,那么第一个终端的进程也一定会终止,如果你想把Pycharm的进程干掉也可以用这种方式。

二 进程的创建

你双击运行你的机器上任何一个程序都是起了一个进程,右击运行你写的Python代码也是起了一个进程。它的本质其实就是向操作系统申请一块内存空间,然后把数据放进去,这样一个进程就起来了。

三 并发工作原理

原来我们按照软件开发规范写的程序,假如里面都有2个任务,但是启动文件只有一个,所以,当你启动程序的时候,其实就是起了一个进程,也就是把这一个进程给了操作系统。如果我们能够同时启动10个同样的进程,把这10个进程都交给操作系统,操作系统就会在这10个进程之间来回切换,这也就实现了并发。如果是四核CPU来处理这10个进程,那么同一时间还会有四个进程可以实现并行。但是把同一个程序运行10次手动开启10个进程,这种方式肯定不是我们所期望的,最好是我们能够调用操作系统的一个接口,在执行第一个任务的时候由操作系统来自动的开启一个进程,同时执行第二个任务,也就是把我们程序中的两个任务放到不同的进程中去,操作系统就会在这两个进程之间来回切换。这两个进程是父子关系,首先运行的是第一个进程,由第一个进程自动的向操作系统申请一块内存空间,把第一个进程所产生的数据放到这个内存空间中,CPU到这个内存空间取数据,这样第二个进程也就起来了,也就是开启了一个子进程,对于操作系统而言,这两个进程都是进程,那么他就会在这两个进程之间切换,这样就能实现并发。
创建新的进程主要有4种方式:

1 系统初始化
2 一个进程在运行过程中开启子进程
3 用户的交互式请求
4 批处理作业的初始化

我们主要研究的就是第二种:一个进程在运行过程中开启子进程。
其实这样的例子我们在上文中已经出现了,在执行Pycharm这个进程的时候又开启了一个Python进程,在执行终端这个进程的时候也是又开启了一个Python进程。很明显开启一个父进程,他的数据是来源于硬盘,而开启一个子进程它的数据是来源于内存。
对于创建子进程来说UNIX系统(MacOS系统和Linux系统都是UNIX系统内核)和Windows系统是有所区别的,UNIX系统会从自己父进程的内存空间完整的拷贝一份给子进程,作为子进程运行的初始状态,所以子进程的初始状态和父进程是一模一样的,但是只要是不同的进程,他们的内存空间在硬件级别就是隔离的。 随着子进程与父进程的运行,假如子进程把数据修改了,对于父进程没有任何影响。Windows系统创建子进程也会拷贝一份父进程内存空间的数据,但是子进程还会初始化一些自己独有的数据,所以,子进程的初始状态并不是和父进程一模一样的。这两者的区别对于我们写程序没有影响,只是他们底层的工作原理不同而已。
只要是开启一个进程就是向操作系统发送请求,调用操作系统的功能,其实也就是调用它的接口,操作系统一般是基于C语言和少量的汇编语言编写的,其实底层调用机器硬件的都是C语言的接口,UNIX系统上调用的新建进程的接口叫做 fork,而Windows系统调用的新建进程的接口叫做 CreateProcess,我们的Python语言是基于C语言编写的,所以我们所使用的Python接口其实就是调用了龟叔用C语言给我们封装好的接口。

四 开启子进程的两种方式

1. 函数

把子进程的任务写成函数,并当作参数传递给多进程类

from multiprocessing import Process
import time
def task(name):  # 如果不传参数,下面调用类的时候args=(...)去掉就可以了
    print('2 %s进程开始' % name)
    time.sleep(3)
    print('3 %s进程结束' % name)
# 在Windows系统上,开启子进程的操作一定要放在这下面操作
if __name__ == '__main__':
    # target参数指定要开启的进程所要执行的任务,args后面的数据类型必须是元祖
    p = Process(target=task, args=('Albert',))
    # p = Process(target=task, kwargs={'name':'Albert'})  # 这两种方式都可以
    p.start()  # 向操作系统发指令,申请内存空间,拷贝数据,创建子进程
    print('1 这是主进程')

p.start()是在主进程运行中给操作系统发了一个信号,发完信号他就继续执行下面的代码,但是对于操作系统而言,要经历一系列过程才能创建这个子进程,所以你会看到我标识的1,2,3的打印顺序,虽然1和2几乎是在同时完成,但他们依然是有先后顺序的。根据这个打印结果,我们可以判断出父进程和子进程在操作系统上的执行是并发执行的,而不是按照我们代码的先后顺序执行的。
由于Windows系统开进程的操作会重新再导入一遍父进程的文件,这也就是说第一次运行到p.start()这行代码的时候,会把以上代码重新倒入,第二次在执行p.start()也是一样的,所以如果你不写那个分支语句就相当于是无限导入,当我们写了这个分支语句__> _name___就不再等于main,而是等于这个模块名,所以分支的条件不成立,就可以正常使用了。因此,在Windows系统上开子进程一定要写在main下面,MacOS系统随意。

2. 类

重写多进程类

from multiprocessing import Process
import time
class MyProcess(Process):
    def __init__(self, name):  # 重写父类init
        super(Process, self).__init__()  # 重用父类init
        self.name = name
    def run(self):  # 重写run方法,把原来的任务写到这里面去
        print('2 %s进程开始' % self.name)
        time.sleep(3)
        print('3 %s进程结束' % self.name)
if __name__ == '__main__':
    p = MyProcess('Albert')  # 可以传参数,自然也可以不传参数,由你掌控
    p.start()  # 调用的就是run方法
    print('1 这是主进程')

这里我们重写Process类,我们并不是十分清楚这个类里面都有哪些东西,所以需要注意重用父类的功能,另外p.start()最后调用的是run这个方法,所以,一定要记住,run方法这个名字不能变。有兴趣的同学可以去看一下,Python这一部分的源码。

五 验证进程内存空间相互隔离

from multiprocessing import Process
import time
x = 10
def task():
    time.sleep(3)
    global x
    x = 0
    print(x,'子进程结束')
if __name__ == '__main__':
    p = Process(target=task, )
    p.start()
    time.sleep(5)  # 保证子进程运行完了,再打印x
    print(x)

根据打印结果先出现0,再出现10,我们就可以判断出,子进程与父进程的内存空间是完全隔离的。

六 父进程等待子进程结束

在有些的场景下,如果子进程还没有结束,我们的父进程并不能往下继续执行代码,比如我们上一阶段学习的基于TCP协议的网络编程,如果子进程TCP连接还没有建好,那么我们肯定是不能让他进行下一步的通信循环的,所以父进程需要等待子进程结束,才能继续执行后面的代码,如果子进程先于父进程结束,父进程虽然也会继续往下执行代码,但是这个父进程也是结束不了的,他依然会等待子进程结束,但是这个等待已经是无意义的等待,因为父进程已经没有什么东西留给子进程了,这是本章节后面会讲到的内容。那么,我们如何实现父进程等待子进程结束呢?
我们之前的处理逻辑如下图所示,是我们事先知道子进程大概运行的时间是3秒多,就让父进程等5秒,但是子进程运行多少时间这并不是我们能够预知的,所以必须要有一种处理机制。

既然子进程是由父进程创建的,那么父进程就能够实现对子进程的动态监测,他内部实现的原理也就是每隔一段时间就询问一下子进程是否结束,动态监测的方法是join()方法。

from multiprocessing import Process
import time
x = 10
def task():
    time.sleep(3)
    global x
    x = 0
    print(x, '子进程结束')
if __name__ == '__main__':
    p = Process(target=task, )
    p.start()
    p.join()  # 父进程动态监测子进程是否结束
    print(x)

到目前为止,我们还没有创建过多个子进程,但是,你现在也应该会了吧

执行代码,查看你电脑上面的打印结果,由于机器的硬件性能不同和正在运行的进程数量多少的区别,你看到的结果可能是按照从0-9的顺序的,也可能不是,可能是主进程12行先打印,也可能在过程中打印,也可能在最后打印,这些都不重要,重要的是它的运行过程,第11行代码不是创建子进程的代码,而是给操作系统发送信号,虽然是在循环里面,但是发10个信号很快,创建子进程也有可能很快,紧跟着就执行了,也可能这一系列过程比较慢,他就会先执行后面的第12行代码。但是多个子进程在操作系统中,执行的先后顺序并非一定要按照收到信号的顺序执行,因为这是由操作系统控制的,除了你这10个进程之外,你的机器上一定还有其他的进程参与,操作系统都一视同仁。本来你的目的就是让这10个子进程并发执行的,那自然也就没有先后顺序这样的说法,如果刚好的你的顺序很整齐,甚至第12行最后才打印,那就是你的电脑CPU运行的速度比较快。其实我想说的是创建进程的开销比较大,但是我电脑运行的比较快,不能作为说明的依据,如果你能看到第12行代码先打印,那么就说明创建进程花费的时间比较长,时间长短都是相对的,与进程相对比的是线程,后面我们还会有更详细的说明。
开10个子进程,并让父进程等待这10个子进程结束

from multiprocessing import Process
import time
def task(n):
    print('%s is running' % n)
    time.sleep(n)
if __name__ == '__main__':
    start_time = time.time()
    p_list = []
    for i in range(10):
        p = Process(target=task, args=(i,))
        p_list.append(p)
        p.start()
    for p in p_list:
        p.join()  # 这个join方法如果写的上面的for循环下面就废了。。。
    print('主进程。。。。。。', time.time() - start_time)

根据打印结果,你就能看得出来,整个程序的运行时间是这里面子进程等待最长的时间再加一点点切换的时间。

七 进程对象的其他属性

这些属性或者方法都是给对象用的,使用起来就很简单了

from multiprocessing import Process
import time
def task(n):
    print('%s is running' % n)
    time.sleep(n)
if __name__ == '__main__':
    # p = Process(target=task, args=(1,))  # 打印默认进程名字
    p = Process(target=task, args=(1,), name='这是个牛逼的进程')  # 也可以给进程起名字
    p.start()
    print(p.pid)  # 在父进程查看子进程pid
    print(p.name)  # 打印进程名字
    p.terminate()  # 父进程干死自己的子进程,这里还是向操作系统发信号
    time.sleep(1)  # 要等一下才能干死
    p.join()  # 保证子进程死了
    print(p.is_alive())  # 判断子进程是否干死了

几个进程id一块打印出来

from multiprocessing import Process
import time, os
def task(n):
    print('self:%s,parent:%s' % (os.getpid(), os.getppid()))
    time.sleep(n)
if __name__ == '__main__':
    p = Process(target=task, args=(1,))
    p.start()
    print('子进程:', p.pid)
    print('主进程:', os.getpid())
    print('主进程的父进程(Pycharm进程PID):', os.getppid())

关于进程这里我们需要说明的一点,Windows系统可以忽略,在Linux系统(一般服务器都是用Linux系统)上,系统刚启动的时候会启动一个init进程,这个进程是所有进程的祖宗,他会在这个进程基础之上再开很多子进程,孙子进程。。。。。。如下图所示

八 僵尸进程与孤儿进程

接下来我们要讲解的内容是以Linux系统为标准,Windows系统是完全不同的进程机制,所以接下来的内容只能靠你去理解了。
我们这次先来看现象,再来说结论。

from multiprocessing import Process
import time, os
def task(name):
    print('%s is running' % name)
    time.sleep(50)
if __name__ == '__main__':
    p = Process(target=task, args=(1,))
    p.start()
    print('主进程', os.getpid())

执行代码,你会发现主进程代码运行完了,但是主进程并没有死,可以用我们本章开始教的命令去验证,也可以在你的Pycharm里面直接看到程序并没有结束。这个现象就是主进程要等待它的子进程结束之后他才会结束,为什么要这么做呢?
举个例子,一个父进程创建了一个子进程,那么父进程一定有查看子进程PID的需求,假如子进程结束之后操作系统就把它的数据全部清空,那么父进程再想查看就查看不到了,所以父进程要查看一定要在子进程结束之前查看,但是父进程并不能知道子进程什么时候结束。所以,这就是一个矛盾点。那么我们怎么解决这个问题呢?
在Linux系统上有一种机制叫做:僵尸进程,僵尸进程就是这个进程结束了,但是它还有东西留在操作系统中,就像僵尸一样,灵魂虽然已经死了,但还有躯体留在世界上。Linux系统上除了init进程之外,所有的进程都是有父进程的,这个僵尸进程指的是:一个进程运行结束之后,首先要做的是把它占用的内存空间全都清理掉,把它打开的文件全都关掉,这些资源全都回收掉,但是会保留这个进程的PID,他占用CPU的时间和他退出的状态等等这些信息,留下这些信息的目的就是父进程需要查看的时候能够查看得到。作为父进程,就需要给子进程收尸,但这一定是不再需要子进程这些信息的时候再做收尸的事,那么什么时候就不再需要这些信息呢?一定是当父进程即将结束的时候,正常情况下,如果你不做任何强制性的操作,父进程就会调用waitpid这个接口来回收子进程的数据,注意:这是Linux系统的接口。所有的子进程都要进入僵尸进程的状态,最后由父进程来完成收尸。
如果子进程没有结束,而父进程先结束了,那么子进程就会变成孤儿进程,孤儿进程也会进入僵尸状态,孤儿自然是无家可归的,这个时候就会由政府出面来对孤儿进程的僵尸状态进行回收,政府就是init进程。 很明显,由父进程来给子进程收尸比由init进程给孤儿进程收尸要好的多,因为init可能需要回收很多的孤儿进程,这样就不能立即处理我们刚刚留下的孤儿进程,而父进程只需要负责处理自己产生的子进程的僵尸进程就可以了。
僵尸进程和孤儿进程有没有什么害处?
孤儿进程其实并没有什么害处,因为总会有一个init进程一直在后面回收,所以,影响并不大。但是对于僵尸进程来说,可能会有这样的场景:一个父进程循环的起了很多的子进程,当子进程运行结束之后,就变成了僵尸进程,而父进程一直没有结束,这样也就一直不会出处理这些大量的僵尸进程,这时并不会占用大量的内存空间,但是对于操作系统来说PID数量是有限的,如果有大量的僵尸进程就意味着进程编号被大量的无用的进程所占用着,再开新的进程就有可能会受影响。所以,僵尸进程是有害的。
僵尸进程的产生是由父进程所导致的,如果真的是父进程需要循环开启子进程而父进程又不能立即结束,那么我们就应该在之前发起waitpid的操作,把子进程回收掉。我们的Python代码并没有waitpid的操作,但是有一个方法里面有,这个方法就是join()。

这个就是龟叔先用C语言把Linux系统上的waitpid接口封装成Python接口,然后又封装在join()方法里面。

九 守护进程

守护进程根据字面意思理解,就是一个进程守护着另外一个进程,当被他守护的进程死了,那么这个守护进程的生命周期也就到头了。在历史上,崇祯皇帝死了,守护他的太监王承恩也就跟着走了。
开两个进程的目的是为了让两个进程去执行两个任务,只不过等到主进程结束之前,守护进程也就跟着结束了。守护进程的生命周期是伴随着主进程整个的生命周期, 主进程的任务干完了,守护进程的任务也要跟着一块结束。
之前我们的代码,主进程即使任务执行完了,也要等待子进程结束才会结束自己的进程。

from multiprocessing import Process
import time
def task(name):
    print('%s is running' % name)
    time.sleep(3)
if __name__ == '__main__':
    p = Process(target=task, args=(1,))
    p.start()
    print('主进程')

现在我希望的是:主进程只要执行的任务一结束,不需要等待子进程任务结束就会结束主进程。那么我们怎么实现呢

from multiprocessing import Process
import time
def task(name):
    print('%s is running' % name)
    time.sleep(3)
if __name__ == '__main__':
    p = Process(target=task, args=(1,))
    p.daemon = True  # 把子进程变成守护进程(儿子刚一出生就把他给腌了,变成太监)
    p.start()
    print('主进程')

守护进程就是守护主进程运行代码,当主进程的把要做的任务都干完了,守护进程也就没有守护的必要了,所以就随着主进程去了。
开子进程的目的是为了能够让主进程和子进程并发执行,守护进程的目的是:当主进程要执行的任务做完了,守护进程也就没有再执行的必要了。当我们出现这样的需求的时候,就应该用守护进程,本章后面的内容会有应用。

十 互斥锁

假如现在有三个任务都在不同的软件中运行,他们都有自己的一篇内容需要打印

from multiprocessing import Process
import time, random
# 假如这个进程是由word程序打印
def task1():
    print('任务1 名字是:英格拉姆')
    time.sleep(random.randint(1, 5))
    print('任务1 性别是:male')
    time.sleep(random.randint(1, 5))
    print('任务1 年龄是:22')
    time.sleep(random.randint(1, 5))
# 这个进程是由notepad++程序打印
def task2():
    print('任务2 名字是:卡戴珊')
    time.sleep(random.randint(1, 5))  # 模拟IO操作
    print('任务2 性别是:female')
    time.sleep(random.randint(1, 5))
    print('任务2 年龄是:25')
    time.sleep(random.randint(1, 5))
# 这个进程是由wps程序打印
def task3():
    print('任务3 名字是:詹姆斯')
    time.sleep(random.randint(1, 5))
    print('任务3 性别是:male')
    time.sleep(random.randint(1, 5))
    print('任务3 年龄是:34')
    time.sleep(random.randint(1, 5))
if __name__ == '__main__':
    p1 = Process(target=task1, )
    p2 = Process(target=task2, )
    p3 = Process(target=task3, )
    p1.start()
    p2.start()
    p3.start()
    print('主进程')

由于三个进程是并发执行的,所以操作系统控制打印来回在多个任务之间切换,最后你看到的打印结果就错乱了。 你是更想要效率还是更想要正确的结果?打印机是怎么工作的?怎么样这个任务就不会乱了?
所以,我们要做的就是把并发变成串行,才能保证不出现错乱。

from multiprocessing import Process
import time, random
# 假如这个进程是由word程序打印
def task1():
    print('任务1 名字是:英格拉姆')
    time.sleep(random.randint(1, 5))  # 模拟IO操作
    print('任务1 性别是:male')
    time.sleep(random.randint(1, 5))
    print('任务1 年龄是:22')
    time.sleep(random.randint(1, 5))
# 这个进程是由notepad++程序打印
def task2():
    print('任务2 名字是:卡戴珊')
    time.sleep(random.randint(1, 5))
    print('任务2 性别是:female')
    time.sleep(random.randint(1, 5))
    print('任务2 年龄是:25')
    time.sleep(random.randint(1, 5))
# 这个进程是由wps程序打印
def task3():
    print('任务3 名字是:詹姆斯')
    time.sleep(random.randint(1, 5))
    print('任务3 性别是:male')
    time.sleep(random.randint(1, 5))
    print('任务3 年龄是:34')
    time.sleep(random.randint(1, 5))
if __name__ == '__main__':
    p1 = Process(target=task1, )
    p2 = Process(target=task2, )
    p3 = Process(target=task3, )
    p1.start()
    p1.join()
    p2.start()
    p2.join()
    p3.start()
    p3.join()
    print('主进程')

但是,我们要做的是并发,所谓的并发就是所有的子进程几乎在同时创建,那么就有个可能p3这个子进程先启动起来了而不是p1,但我们现在的操作是人为的把子进程按照先后的顺序执行, 显然这么做是不合理的。应该是谁先抢这个资源就由谁先使用,而且是直到第一个使用者使用结束,后面的使用者再继续争抢,以此循环,这就像是大学一个寝室的同学使用洗手间一样,谁先进去就一定会上一把锁在门上,直到他出来才会打开这个锁,其他人再进去的时候,只要拿到了这个锁,那么就掌握了主动权。所以说,要保证后面的人进不去,执行进程的关键其实就在于这把锁。

from multiprocessing import Process, Lock
import time, random
mutex = Lock()  # 实例化锁的对象 
# 假如这个进程是由word程序打印
def task1(lock):
    lock.acquire()  # 上锁
    print('任务1 名字是:英格拉姆')
    time.sleep(random.randint(1, 5))
    print('任务1 性别是:male')
    time.sleep(random.randint(1, 5))
    print('任务1 年龄是:22')
    time.sleep(random.randint(1, 5))
    lock.release() # 解锁 
# 这个进程是由notepad++程序打印
def task2(lock):
    lock.acquire()  # 如果连续两次抢锁,第一次抢到了,接着抢一定抢不到,那么程序就卡了
    # lock.acquire()
    print('任务2 名字是:卡戴珊')
    time.sleep(random.randint(1, 5))
    print('任务2 性别是:female')
    time.sleep(random.randint(1, 5))
    print('任务2 年龄是:25')
    time.sleep(random.randint(1, 5))
    lock.release()
# 这个进程是由wps程序打印
def task3(lock):
    lock.acquire()
    print('任务3 名字是:詹姆斯')
    time.sleep(random.randint(1, 5))
    print('任务3 性别是:male')
    time.sleep(random.randint(1, 5))
    print('任务3 年龄是:34')
    time.sleep(random.randint(1, 5))
    lock.release()
if __name__ == '__main__':
    p1 = Process(target=task1, args=(mutex,))
    p2 = Process(target=task2, args=(mutex,))
    p3 = Process(target=task3, args=(mutex,))
    p1.start()
    p2.start()
    p3.start()
    print('主进程')

注意:每个子进程抢的都是同一把锁,但是子进程之间的内存空间又是完全隔离的,所以我们只能通过参数的形式把这把锁传递给子进程。你看到的结果就是只要有任何一个进程先起来了,那么就一定会等这个进程的任务执行完了其他的进程才能进来。此时程序的执行是三个进程都起来了,但为了保证打印内容的准确,依然是把并发变成了串行,这一点与join类似。与区别在于:join是人为的规定好顺序, 现在是所有的进程有公平竞争的机会。 这把锁就叫做互斥锁,所有的进程相互排斥,同一时间只能由一个进程掌控。

十一 IPC机制

IPC(Inter Process Communication)机制就是进程间的通信,进程之间内存是隔离的,不能直接通信,我们知道的可以用一个中间介质比如硬盘来完成这个过程,但是这样的效率太低了。硬盘之所以能够解决是因为硬盘上的文件是可以共享的, 但是如果我们能够在内存留一个所有进程共享的内存空间,那么这个问题就可以完美的解决了 。确实有这样一个内存空间,但是!!!我要说的是:紧接着我要讲的东西了解就好了,我不会深入说明,因为这个设计有缺陷,你如果用了,极有可能会给你的程序带来bug,所以知道有这个东西就好了。

_from _multiprocessing > _import _Manager就是使用这个Manager类,可以造共享的内存空间,但是千万别用!!!

Manager之所以说它有缺陷就是因为没有处理好锁的问题,会给数据安全带来极大的隐患。 所以如果一定要用的话需要自己处理好锁的问题,你自己的在处理的时候一定要小心谨慎, 甚至你可能会想到所有的代码全部加锁,那还不如程序直接变成串行。如果这个锁处理不好,就极有可能出现死锁现象,把整个程序都卡在原地,所以最好是能自动来帮我们处理好锁的问题。

IPC机制需要三件事:
    1 找一块所拥有进程共享的空间
    2 这个空间必须是内存空间
    3 帮我们自动处理好锁的问题

这个机制就是Queue,他指的是队列,队列是先进先出。它造出来的对象就能符合我们以上三点。

from multiprocessing import Queue
q = Queue(3)  # 参数3指的是排队的有3个人
q.put("1")  # 三个人一次排队
q.put("2")
q.put("3")
print(q.get())
print(q.get())
print(q.get())
# print(q.get())  # 只有三个人排队,取不出来第四个人,程序卡死

需要注意的是:
第一点:队列用来存储进程间沟通的消息,数据量不应该过大。就像是我作为老板在北京给你打电话,要求你去盖一个房子,我只是打电话告诉你就好了,不会把钢筋水泥都寄给你。所以队列之间的通信又常常被人们称为消息队列。
第二点:刚才进程之间通信的参数我们设置成3,但是如果你写成300000这是没有意义的,这会局限于你的内存的限制。
put有两个默认参数,是block=True和timeout=None,分别指的是阻塞和超时时间。

from multiprocessing import Queue
q = Queue(3)
q.put("1", block=True)  # 这样表示默认会阻塞
q.put("2", block=True)
q.put("3", block=True)
q.put("4", block=True, timeout=3)  # 3秒之后结束阻塞,程序报错,如果没有timeout就会一直阻塞

要是把True改成False呢?

from multiprocessing import Queue
q = Queue(3)
q.put("1", block=False)  # 前三个都不会阻塞,True或者False都无所谓
q.put("2", block=True)
q.put("3", block=True)
q.put("4", block=False)  # 不阻塞直接报错

get同理,只是反过来了。
进程之间的通信,我们一般就是用这种方式,这就是队列或者叫Queue。

十一 生产者消费者模型

从这个模型你就能够看出来,这是一种写程序的思路。该模型包含两类重要的角色:

1 生产者:将负责造数据的任务比喻为生产者。
2 消费之:将负责处理生产者造出来的数据的任务比喻为消费者。

在生活这样的例子非常常见
肯德基的快餐之所以快就是因为厨师只负责生产食物,然后将生产好的食物放在吧台,而作为消费者只需要负责吃就好了,两者之间互补影响。
如果是我们之前的工作方式,同时进行的程序只能有一个,要么吃,要么生产,但是现在我们有了多进程,这个任务可以同时进行,在程序的执行中,再一次实现了程序的解耦合。

import time
import random
from multiprocessing import Process, Queue
def consumer(name, q):
    while True:
        res = q.get()
        time.sleep(random.randint(1, 3))
        print('\033[46m消费者===》%s 吃了 %s\033[0m' % (name, res))
def producer(name, q, food):
    for i in range(5):
        time.sleep(random.randint(1, 2))
        res = '%s%s' % (food, i)
        q.put(res)
        print('\033[45m生产者者===》%s 生产了 %s\033[0m' % (name, res))
if __name__ == '__main__':
    # 1、共享的吧台
    q = Queue()
    # 2、生产者们
    p1 = Process(target=producer, args=('Albert主厨', q, '新疆大盘鸡'))
    p2 = Process(target=producer, args=('厨神', q, '扬州烤鹅'))
    p3 = Process(target=producer, args=('主厨小迷弟', q, '南京回锅肉'))
    # 3、消费者们
    c1 = Process(target=consumer, args=('孙悟空', q))
    c2 = Process(target=consumer, args=('猪八戒', q))
    p1.start()
    p2.start()
    p3.start()
    c1.start()
    c2.start()

实现生产者消费者模型的三要素:

  1. 生产者
  2. 消费者
  3. 队列

注意:生产者消费者模型只是一种解决问题或者写代码的思路,没有明确表示一定是用什么技术。
什么时候用该模型?
程序中出现明显的两类任务,一类任务是负责生产数据,另一类任务是负责处理生产的数据,最明显的例子就是爬虫和数据分析的组合。
用该模型有什么好处?

  1. 实现了生产者与消费者解耦和
  2. 平衡了生产力与消费力,即生产者可以一直不停地生产,消费者可以不停地处理,因为二者不再直接沟通的,而是跟队列沟通

十二 守护进程的应用

其实以上的写的生产者消费者模型的程序还存在一个明显的问题,程序一直不会结束,也就是卡死了,我们把程序简化一下来分析。

import time
import random
from multiprocessing import Process, Queue
def consumer(name, q):
    while True:
        res = q.get()
        time.sleep(random.randint(1, 3))
        print('\033[46m消费者===》%s 吃了 %s\033[0m' % (name, res))
def producer(name, q, food):
    for i in range(5):
        time.sleep(random.randint(1, 2))
        res = '%s%s' % (food, i)
        q.put(res)
        print('\033[45m生产者者===》%s 生产了 %s\033[0m' % (name, res))
if __name__ == '__main__':
    q = Queue()
    p1 = Process(target=producer, args=('Albert主厨', q, '新疆大盘鸡'))
    c1 = Process(target=consumer, args=('猪八戒', q))
    p1.start()
    c1.start()

很明显我们看到的结果是主进程还没有结束,那只有两种情况:1 主进程没有结束,2 子进程没有结束导致了主进程没有结束。从代码上看主进程明显已经执行完了它的代码,作为生产者的子进程也执行完了它的代码,那么唯一的问题就是作为消费者者进程没有结束,进一步分析就会发现q.get()已经没有数据了,所以造成了消费者子进程的阻塞,进而主进场要等待子进程的结束。
可能你首先会想到的解决方案是在res=q.get()后面一行代码加上一个条件分支,如果他为空就退出循环,但是问题是q.get()不是拿到空的数据,而是拿不到数据,所以你添加的那一行代码根本就走不到,这是无用功。如果是把默认参数block改成False那显然也是不对的,只要消费者拿不到数据就会报错。再来想一下加一个timeout,你写多少秒好呢?这显然也不是一个合理的方式。消费者不知道什么时候生产者的数据生产完了,就一直在那傻等,但是生产者生产的,他知道啊,所以生产者生产完了,给消费者一个明显的结束信号,消费者在收到这个信号之后就知道,嗷,没得吃了。

import time
import random
from multiprocessing import Process, Queue
def consumer(name, q):
    while True:
        res = q.get()
        if res is None:
            break
        time.sleep(random.randint(1, 3))
        print('\033[46m消费者===》%s 吃了 %s\033[0m' % (name, res))
def producer(name, q, food):
    for i in range(5):
        time.sleep(random.randint(1, 2))
        res = '%s%s' % (food, i)
        q.put(res)
        print('\033[45m生产者者===》%s 生产了 %s\033[0m' % (name, res))
    q.put(None)  # 信号一定要放最后,信号随便什么都可以,但一定不要与生产者的数据冲突
if __name__ == '__main__':
    q = Queue()
    p1 = Process(target=producer, args=('Albert主厨', q, '新疆大盘鸡'))
    c1 = Process(target=consumer, args=('猪八戒', q))
    p1.start()
    c1.start()

程序写到这里,你一定以为快要结束了,然而,还没完,我们现在再把三个消费者两个生产者加上,你再看看有没有什么问题。

import time
import random
from multiprocessing import Process, Queue
def consumer(name, q):
    while True:
        res = q.get()
        if res is None:
            break
        time.sleep(random.randint(1, 3))
        print('\033[46m消费者===》%s 吃了 %s\033[0m' % (name, res))
def producer(name, q, food):
    for i in range(5):
        time.sleep(random.randint(1, 2))
        res = '%s%s' % (food, i)
        q.put(res)
        print('\033[45m生产者者===》%s 生产了 %s\033[0m' % (name, res))
    q.put(None)  # 信号一定要放最后,信号随便什么都可以,但一定不要与生产者的数据冲突
if __name__ == '__main__':
    q = Queue()
    p1 = Process(target=producer, args=('Albert主厨', q, '新疆大盘鸡'))
    p2 = Process(target=producer, args=('厨神', q, '扬州烧鹅'))
    p3 = Process(target=producer, args=('主厨的小迷弟', q, '南京回锅肉'))
    c1 = Process(target=consumer, args=('孙悟空', q))
    c2 = Process(target=consumer, args=('猪八戒', q))
    p1.start()
    p2.start()
    p3.start()
    c1.start()
    c2.start()

仔细看你就会发现,两位吃货一定要还有什么东西没吃完,浪费时绝对不允许的。那要怎么解决呢?
先来分析一下,生产者是多进程生产的,我们在其中某一个进程结束的时候,最后生产了一个None,但是其他进程可能还没有结束,所以个这个None并一定会出现在队列的末尾,怎么解决?
第一个有点Low的解决方案:

import time
import random
from multiprocessing import Process, Queue
def consumer(name, q):
    while True:
        res = q.get()
        if res is None:
            break
        time.sleep(random.randint(1, 3))
        print('\033[46m消费者===》%s 吃了 %s\033[0m' % (name, res))
def producer(name, q, food):
    for i in range(5):
        time.sleep(random.randint(1, 2))
        res = '%s%s' % (food, i)
        q.put(res)
        print('\033[45m生产者者===》%s 生产了 %s\033[0m' % (name, res))
if __name__ == '__main__':
    q = Queue()
    p1 = Process(target=producer, args=('Albert主厨', q, '新疆大盘鸡'))
    p2 = Process(target=producer, args=('厨神', q, '扬州烧鹅'))
    p3 = Process(target=producer, args=('主厨的小迷弟', q, '南京回锅肉'))
    c1 = Process(target=consumer, args=('孙悟空', q))
    c2 = Process(target=consumer, args=('猪八戒', q))
    p1.start()
    p2.start()
    p3.start()
    c1.start()
    c2.start()
    # 保证生产者生产完毕后,在队列末尾添加结束信号
    p1.join()
    p1.join()
    p1.join()
    # 有几个生产者就应该有几个结束信号
    q.put(None)
    q.put(None)

最后这就是Boss了,出绝招了,接招吧!
除了有Queue之外,我们还有一个JoinalbeQueue,这就像是一个Queue对象,但是他有一个q.join()的操作,可以实现等待q结束。q的结束一定是从生产者不再生产开始计数,直到消费者全部取走,这才算是队列为空。 如果生产者没有生产结束,消费者一直取,但是生产者一直在往里面放,这样肯定是没有意义的。 所以,q.join()依然是放到三个生产者结束后面。除此之外,还有一个q.task_done()操作,表示消费者使用此方法发出信号,表示q.get()的返回项目已经被处理。
所以综合一下:
q.join()生产者生产全部生产结束之后调用此方法进行阻塞,直到消费者把队列中所有的数据全部处理完,阻塞状态将持续到队列中的每个项目均调用q.task_done()方法为止。

import time
import random
from multiprocessing import Process, JoinableQueue
def consumer(name, q):
    while True:
        res = q.get()
        if res is None:
            break
        time.sleep(random.randint(1, 3))
        print('\033[46m消费者===》%s 吃了 %s\033[0m' % (name, res))
        q.task_done()
def producer(name, q, food):
    for i in range(5):
        time.sleep(random.randint(1, 2))
        res = '%s%s' % (food, i)
        q.put(res)
        print('\033[45m生产者者===》%s 生产了 %s\033[0m' % (name, res))
if __name__ == '__main__':
    q = JoinableQueue()
    p1 = Process(target=producer, args=('Albert主厨', q, '新疆大盘鸡'))
    p2 = Process(target=producer, args=('厨神', q, '扬州烧鹅'))
    p3 = Process(target=producer, args=('主厨的小迷弟', q, '南京回锅肉'))
    c1 = Process(target=consumer, args=('孙悟空', q))
    c2 = Process(target=consumer, args=('猪八戒', q))
    p1.start()
    p2.start()
    p3.start()
    c1.start()
    c2.start()
    p1.join()
    p1.join()
    p1.join()
    q.join()
    print('主进程结束')

至此,我们可主进程已然结束,可消费者子进程却还在傻傻的等着吃,这时我们可以确定生产者不再生产了并且队列里面也没有了,这样的等待时没有意义的,那么我们就把它变成守护进程,因为主进程的任务都干完了就能确保队列里面没有数据了,那也就无需再等。

import time
import random
from multiprocessing import Process, JoinableQueue
def consumer(name, q):
    while True:
        res = q.get()
        if res is None:
            break
        time.sleep(random.randint(1, 3))
        print('\033[46m消费者===》%s 吃了 %s\033[0m' % (name, res))
        q.task_done()
def producer(name, q, food):
    for i in range(5):
        time.sleep(random.randint(1, 2))
        res = '%s%s' % (food, i)
        q.put(res)
        print('\033[45m生产者者===》%s 生产了 %s\033[0m' % (name, res))
if __name__ == '__main__':
    q = JoinableQueue()
    p1 = Process(target=producer, args=('Albert主厨', q, '新疆大盘鸡'))
    p2 = Process(target=producer, args=('厨神', q, '扬州烧鹅'))
    p3 = Process(target=producer, args=('主厨的小迷弟', q, '南京回锅肉'))
    c1 = Process(target=consumer, args=('孙悟空', q))
    c2 = Process(target=consumer, args=('猪八戒', q))
    c1.daemon = True  # 子进程变成守护进程
    c2.daemon = True
    p1.start()
    p2.start()
    p3.start()
    c1.start()
    c2.start()
    # 确定生产者确实生产完毕
    p1.join()
    p1.join()
    p1.join()
    # 生产者生产完毕之后,拿到队列中肯定有还有数据,直到这个数据量变为0,q.join()这行代码才算是运行完毕
    q.join()
    # q.join()一旦结束就意味着队列确实为空,消费者已经确实把数据都取干净了。
    print('主进程结束')

总结:
本章僵尸进程与孤儿进程只需了解即可,Manager的用法无需研究,这是没有意义的,Queue的用法是一个重点,这是进程间通信的核心。生产者消费者模型非常重要,这也是设计模式之一,这种设计模式源自于Java,叫做工厂模式,但既然是设计模式那就意味着是一种解决问题的思路,Python依然可以把他发扬光大。

posted @ 2019-04-09 18:26  马一特  阅读(90)  评论(0编辑  收藏  举报