决战Python之巅(十七)-并发三巨头之进程

理论

操作系统相关知识

进程,顾名思义即正在执行的一个过程。进程是对正在运行程序的一个抽象。
进程的概念起源于操作系统,是操作系统最核心的概念,也是操作系统提供的最古老的也是最重要的抽象概念之一。操作系统里的其他内容都是围绕进程的概念展开的。
所以想要了解进程,就必须要对操作系统有一定的了解。
这里就大致介绍一些必备的基础,其他的大家自行百度吧~这里就不详述它的发展史。

操作系统的应用

对于我们程序员来说,我们无法把操作系统所有的硬件操作细节都了解到位。管理并且合理应用这些硬件是项非常繁琐的工作,而有了操作系统,我们就可以从那些繁琐的硬件操作工作中解脱出来,只需要考虑自己的应用软件的编写即可,应用软件直接使用操作系统提供的功能来间接使用硬件。

  • 操作系统隐藏了复杂的硬件接口,提供了良好的抽象接口;
  • 操作系统管理、调度进程,并且将多个进程对硬件的竞争变得有序;

多道技术

在很久很久…以前,计算机只有单核,为了实现并发,就产生了多道技术。
多道技术简单来说就是在多个程序同时运行的前提下,为了合理使用硬件,当一道程序因I/O请求而暂停运行时,CPU便立即转去运行另一道程序。当然,在切换之前会先将进程的状态保存下来,这样就能保证下次切换回来时,能基于上次切走的位置继续运行。(这里就大致介绍一下,真正的多道技术远比这个复杂,可自行百度~)。

进程

什么是进程?
狭义上来说,就是正在运行的程序
广义上来说,进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元
进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈则是存储着活动过程调用的指令和本地变量。
进程是一个“执行中的程序”。只有在操作系统执行这个程序的时候,它才能成为一个活动的实体,我们称之为进程。
这里需要注意的是程序和进程之间区别:
程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
而进程是程序在处理器上的一次执行过程,它是一个动态的概念。
程序是永久的,而进程是暂时的。

还需注意的是:同一个程序执行两次,就会在操作系统中出现两个进程,所有我们可以同时运行一个软件,分别做不同的事情也不会混乱。(就像开了两个word,一个写诗,一个写小说,两个的数据不会乱。)

进程的并行与并发

并行:并行是指两者同时执行,比如赛跑,两个人都在不停地往前跑。(这需要多核…单核CPU不能实现并行,只有一条路,每次只能跑一个)。
并发:并发是指资源有限的情况下,两者交替轮流使用资源。比如在单核CPU上,同时只能执行一个程序,A执行了一段时间后,让给B执行,B执行一段时间后再让给A,交替使用,提高资源利用率。

进程的状态

在程序运行过程中,由于被操作系统的调度算法控制,程序会进入几个状态:就绪、运行和阻塞。

就绪

万事俱备,只欠CPU。此时进程已经拿到除CPU以外的所有必要的资源,只要一拿到CPU就可以立即执行,为所欲为,而此时的进程状态就被称为就绪。

运行

进程拿到了梦寐以求的CPU,程序正在CPU上执行。

阻塞

正在执行的进程,由于需要等待某个事件的发生而无法继续执行是,便被CPU放弃而处于阻塞状态。引起阻塞的情况有很多,例如,等待I/O完成、申请缓冲区不能满足、等待信件(信号)等。
当它等待的事件完成后,便会再度进入就绪状态。
在这里插入图片描述
从代码上来看就是:
在这里插入图片描述

同步和异步

同步:一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成时,依赖的任务才能算完成,这是一种可靠的任务序列。要么都成功要么都失败,两个任务的状态保持一致。
异步:不需要等待被依赖的任务完成,只是通知被依赖的任务要干什么工作,依赖的任务也立即执行,只要自己的任务完成就算完成。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。

阻塞与非阻塞

阻塞和非阻塞这两个概念与程序等待消息通知(无所谓同步或者异步)时的状态有关。
阻塞就是在等待消息通知时什么也不干,就等着你消息通知。
就好比你在下小电影,下的时候什么也不干,就眼巴巴的等着‘叮~’的一声,提示你下完了。
而非阻塞呢,就是在你下小电影的时候,顺便去刷刷微博什么的。

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

1.同步阻塞

效率最低。

2.异步阻塞

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

3.同步非阻塞

需要在不同的行为之间来回切换,效率低下。

4.异步非阻塞

效率高。
Emmmmm,这里的话,这几种形式由于我自己还没确切的搞清楚,所以等以后弄明白了再来补充~~

进程的创建与结束

进程的创建

对于通用系统,需要跑很多应用程序,这就需要有系统运行过程中创建或撤销进程的能力,创建新的进程有4种形式:

  • 1.系统初始化(查看进程linux中用ps命令,windows中用任务管理器,前台进程负责与用户交互,后台运行的进程与用户无关,运行在后台并且只在需要时才唤醒的进程,称为守护进程,如电子邮件、web页面、新闻、打印);
  • 2.一个进程在运行过程中开启了子进程;
  • 3.用户的交互式请求,而创建一个新进程;
  • 4.一个批处理作业的初始化(只在大型机的批处理系统中应用);
    注意,无论哪一种,新进程的创建都是由一个已经存在的进程执行额一个用于创建进程的系统调用而创建的。

进程的结束

  • 1.正常结束;
  • 2.出错退出;
  • 3.严重错误;
  • 4.被其他进程杀死;

Python中的进程操作

之前已经了解到,运行中的程序就是一个进程。所有的进程都是通过它的父进程来创建的。因此,运行起来的Python程序也是一个进程,那么我们也可以在这个程序中创建进程。多个进程可以实现并发的效果,也就是说,当我们的程序中存在多个进程的时候,在某些情况下,可以让程序执行速度加快。Python之所以强大,就是因为它多元化的模块,而进程这里我们要用到的模块式multiprocessing模块。

multiprocessing模块

其实精确的说,multiprocessing不是一个模块而是Python中一个操作、管理进程的包。在这个包中几乎包含了和进程有关的所有子模块,这下子模块大致上可分为四个部分:创建进程部分、进程同步部分、进程池部分、进程之间数据共享。

multiprocessing.process模块

process模块介绍

借用process模块,可以完成进程的创建。

Process([group [, target [, name [, args [, kwargs]]]]]),由该类实例化得到的对象,表示一个子进程中的任务(尚未启动)

强调:
1. 需要使用关键字的方式来指定参数
2. args指定的为传给target函数的位置参数,是一个元组形式,必须有逗号

参数介绍:
1 group参数未使用,值始终为None
2 target表示调用对象,即子进程要执行的任务
3 args表示调用对象的位置参数元组,args=(1,2,'Kris',)
4 kwargs表示调用对象的字典,kwargs={'name':'Kris','age':18}
5 name为子进程的名称
p = process(target=方法名,args=(参数1,参数2,....))
相关方法: 
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开启的进程

相关属性:
1 p.daemon:默认值为False,如果设为True,代表p为后台运行的守护进程,当p的父进程终止时,p也随之终止,并且设定为True后,p不能创建自己的新进程,必须在p.start()之前设置
2 p.name:进程的名称
3 p.pid:进程的pid

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

使用process模块

在一个python进程中开启子进程,start方法和并发效果。

import time
from multiprocessing import Process

def f(name):
    print('hello', name)
    print('我是子进程')

if __name__ == '__main__':
    p = Process(target=f, args=('bob',))
    p.start()
    time.sleep(1)
    print('执行主进程的内容了')

执行结果如下:
在这里插入图片描述
join方法

import time
from multiprocessing import Process

def f(name):
    print('hello', name)
    time.sleep(1)
    print('我是子进程')


if __name__ == '__main__':
    p = Process(target=f, args=('bob',))
    p.start()
    p.join()
    print('我是父进程')

执行结果如下:
在这里插入图片描述
注意:第一段代码和第二段代码的区别,执行结果看起来一样,实际上是不一样的,第一段代码中主进程给操作系统提交了开启子进程的任务,然后休息了一秒钟,在这一秒内,操作系统开启了子进程并执行了子进程部分的代码。第二段代码中主进程给操作系统提交了开启子进程的任务后,由于join方法,主进程需要等待子进程执行完成,才能继续执行。
查看主进程和子进程的进程号

import os
from multiprocessing import Process

def f(x):
    print('子进程id :',os.getpid(),'父进程id :',os.getppid())
    return x*x

if __name__ == '__main__':
    print('主进程id :', os.getpid())
    p_lst = []
    for i in range(5):
        p = Process(target=f, args=(i,))
        p.start()

执行结果如下:
在这里插入图片描述
这里查看pid想说明的是,一个父进程可以开启多个子进程,且子进程之间的内存空间相互隔离。

进程进阶-多进程

多个进程同时运行时,注意,子进程的执行顺序不是根据启动顺序决定的。

import time
from multiprocessing import Process


def f(name):
    print('hello', name)
    time.sleep(1)


if __name__ == '__main__':
    p_lst = []
    for i in range(5):
        p = Process(target=f, args=(i,))
        p.start()

执行结果(多次执行有多种结果,这里只贴一个):
在这里插入图片描述
如果有顺序执行的需求,就需要用到上面讲的join方法:

import time
from multiprocessing import Process

def f(name):
    print('hello', name)
    time.sleep(1)

if __name__ == '__main__':
    p_lst = []
    for i in range(5):
        p = Process(target=f, args=(i,))
        p.start()
        p_lst.append(p)
        p.join()       #方式一 
    # [p.join() for p in p_lst]        #方式二
    print('父进程在执行')

执行结果:
在这里插入图片描述

进程进阶-开启进程的另一种方式

这种方式是以继承Process类的形式开启进程。

import os
from multiprocessing import Process

class MyProcess(Process):
    def __init__(self,name):
        super().__init__()
        self.name=name
    def run(self):
        print(os.getpid())
        print('%s 正在和女主播聊天' %self.name)
if __name__ == '__main__':
	p=MyProcess('xxx')
	p.start() #start会自动调用run
	p.join()
	print('主线程')

进程进阶-数据隔离

#多进程中说过各个进程之间的内存空间隔离,这也就导致了数据隔离。
from multiprocessing import Process

def work():
    global n
    n=0
    print('子进程内: ',n)

if __name__ == '__main__':
    n = 100
    p=Process(target=work)
    p.start()
    p.join()
    print('主进程内: ',n)

执行结果:
在这里插入图片描述
子进程中已声明调用全局变量n,并做修改,但是主进程中的n仍为原值。

守护进程

守护进程会随着守护的进程结束而结束。
主进程创建守护进程:
  其一:守护进程会在主进程代码执行结束后就终止;
  其二:守护进程内无法再开启子进程,否则抛出异常;
注意:进程之间是互相独立的,主进程代码运行结束,守护进程随即终止。

import time
from multiprocessing import Process


def foo():
	print(123)
	time.sleep(1)
	print("end123")


if __name__ == '__main__':
	p1 = Process(target=foo)
	p1.daemon = True
	p1.start()
	time.sleep(0.1)
	print("main-------")#打印该行则主进程代码结束,则守护进程p1应该被终止.
#可能会有p1任务执行的打印信息123,因为主进程打印main----时,p1也执行了,但是随即被终止.

执行结果:
在这里插入图片描述

进程同步(multiprocessing.Lock)

从前面可以看出,一旦开启多进程,它们之间运行并没有顺序,而且开启后也不受我们控制,尽管并发编程能让我们更加充分利用IO资源,但也带来新问题。
当多个进程使用同一份数据资源时候,就会发生数据安全或顺序混乱的问题。
例如本地文件中有一个数据a(假设a初始值为0),每次进程调用都是让它加1,如此看来,开启5个进程运行结束后,文件中a的值应该为5。然而实际上并非如此,当进程1拿到a(a=0)后,需要先计算再写会文件,就在进程1计算或写会的过程中,进程2也有可能拿到了a,而这时候进程1还没将计算完的结果写回去即此时a还是0,这样就导致进程1、进程2最终往文件中写的都是a=1。
这时候,就需要用到锁:

# 由并发变成了串行,牺牲了运行效率,但避免了竞争
import os
import time
import random
from multiprocessing import Process,Lock

def work(lock,n):
    lock.acquire()
    print('%s: %s is running' % (n, os.getpid()))
    time.sleep(random.random())
    print('%s: %s is done' % (n, os.getpid()))
    lock.release()
if __name__ == '__main__':
    lock=Lock()
    for i in range(3):
        p=Process(target=work,args=(lock,i))
        p.start()

执行结果:
在这里插入图片描述
上面这种情况虽然使用加锁的形式实现了顺序的执行,但是程序又重新变成串行了,这样确实会浪费了时间,却保证了数据的安全。
注意:锁与join的区别,虽然最后都是串行,但是锁可以使部分代码串行。

加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行的修改,没错,速度是慢了,但牺牲了速度却保证了数据安全。
虽然可以用文件共享数据实现进程间通信,但问题是:
1.效率低(共享数据基于文件,而文件是硬盘上的数据);
2.需要自己加锁处理;

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

进程通信(multiprocessing.Queue)

当我们需要在多进程中传递数据(内存中的数据),即需要进程间通信-IPC。

队列

创建共享的进程队列,Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。

Queue([maxsize]) 
创建共享的进程队列。
参数 :maxsize是队列中允许的最大项数。如果省略此参数,则无大小限制。
底层队列使用管道和锁定实现。
#相关方法:
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()方法可以禁止这种行为。

队列-代码示例

'''
multiprocessing模块支持进程间通信的两种主要形式:管道和队列
都是基于消息传递实现的,但是队列接口
'''

from multiprocessing import Queue
q=Queue(3)

#put ,get ,put_nowait,get_nowait,full,empty
q.put(3)
q.put(3)
q.put(3)
# q.put(3)   # 如果队列已经满了,程序就会停在这里,等待数据被别人取走,再将数据放入队列。
           # 如果队列中的数据一直不被取走,程序就会永远停在这里。
try:
    q.put_nowait(3) # 可以使用put_nowait,如果队列满了不会阻塞,但是会因为队列满了而报错。
except: # 因此我们可以用一个try语句来处理这个错误。这样程序不会一直阻塞下去,但是会丢掉这个消息。
    print('队列已经满了')

# 因此,我们再放入数据之前,可以先看一下队列的状态,如果已经满了,就不继续put了。
print(q.full()) #满了

print(q.get())
print(q.get())
print(q.get())
# print(q.get()) # 同put方法一样,如果队列已经空了,那么继续取就会出现阻塞。
try:
    q.get_nowait(3) # 可以使用get_nowait,如果队列满了不会阻塞,但是会因为没取到值而报错。
except: # 因此我们可以用一个try语句来处理这个错误。这样程序不会一直阻塞下去。
    print('队列已经空了')

print(q.empty()) #空了

注意,队列是先进先出的,即先put进去的数据,先被get出来。

队列-进程间通信

import time
from multiprocessing import Process, Queue

def f(q):
    q.put([time.asctime(), 'from Eva', 'hello'])  #调用主函数中p进程传递过来的进程参数 put函数为向队列中添加一条数据。

if __name__ == '__main__':
    q = Queue() #创建一个Queue对象
    p = Process(target=f, args=(q,)) #创建一个进程
    p.start()
    print(q.get())
    p.join()

生产者消费者模型

在实际的软件开发过程中,经常会碰到如下场景:某个模块负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、线程、进程等)。产生数据的模块,就形象地称为生产者;而处理数据的模块,就称为消费者。
单单抽象出生产者和消费者,还够不上是生产者/消费者模式。该模式还需要有一个缓冲区处于生产者和消费者之间,作为一个中介。生产者把数据放入缓冲区,而消费者从缓冲区取出数据。
而队列就可以充当这个缓冲区。

生产者消费者模型-Queue初始版
from multiprocessing import Process,Queue
import time,random,os
def consumer(q):
    while True:
        res=q.get()
        time.sleep(random.randint(1,3))
        print('\033[45m%s 吃 %s\033[0m' %(os.getpid(),res))

def producer(q):
    for i in range(10):
        time.sleep(random.randint(1,3))
        res='包子%s' %i
        q.put(res)
        print('\033[44m%s 生产了 %s\033[0m' %(os.getpid(),res))

if __name__ == '__main__':
    q=Queue()
    #生产者们:即厨师们
    p1=Process(target=producer,args=(q,))

    #消费者们:即吃货们
    c1=Process(target=consumer,args=(q,))

    #开始
    p1.start()
    c1.start()
    print('主')

这里主进程永远不会结束,原因是:生产者p在生产完后就结束了,但是消费者c在取空了q之后,则一直处于死循环中且卡在q.get()这一步。
解决方式无非是让生产者在生产完毕后,往队列中再发一个结束信号,这样消费者在接收到结束信号后就可以break出死循环。

生产者消费者模型-Queue改良版
#版本一:(结束信号由生产者发出)
from multiprocessing import Process,Queue
import time,random,os
def consumer(q):
    while True:
        res=q.get()
        if res is None:break #收到结束信号则结束
        time.sleep(random.randint(1,3))
        print('\033[45m%s 吃 %s\033[0m' %(os.getpid(),res))

def producer(q):
    for i in range(10):
        time.sleep(random.randint(1,3))
        res='包子%s' %i
        q.put(res)
        print('\033[44m%s 生产了 %s\033[0m' %(os.getpid(),res))
    q.put(None) #发送结束信号
if __name__ == '__main__':
    q=Queue()
    #生产者们:即厨师们
    p1=Process(target=producer,args=(q,))

    #消费者们:即吃货们
    c1=Process(target=consumer,args=(q,))

    #开始
    p1.start()
    c1.start()
    print('主')

#版本二:(结束信号由主进程发书)
from multiprocessing import Process,Queue
import time,random,os
def consumer(q):
    while True:
        res=q.get()
        if res is None:break #收到结束信号则结束
        time.sleep(random.randint(1,3))
        print('\033[45m%s 吃 %s\033[0m' %(os.getpid(),res))

def producer(q):
    for i in range(2):
        time.sleep(random.randint(1,3))
        res='包子%s' %i
        q.put(res)
        print('\033[44m%s 生产了 %s\033[0m' %(os.getpid(),res))

if __name__ == '__main__':
    q=Queue()
    #生产者们:即厨师们
    p1=Process(target=producer,args=(q,))

    #消费者们:即吃货们
    c1=Process(target=consumer,args=(q,))

    #开始
    p1.start()
    c1.start()

    p1.join()
    q.put(None) #发送结束信号
    print('主')

但是,当有多个生产者时候,这种方法就需要我们多次join等待生产结束,然后在主进程中根据消费者的个数发送相应次数的结束信号。

生产者消费者模型-JoinableQueue版

为了解决多消费者生产者的问题,我们可以使用JoinableQueue:
创建可连接的共享进程队列。这就像是一个Queue对象,但队列允许项目的使用者通知生产者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。

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

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

q.join() 
生产者将使用此方法进行阻塞,直到队列中所有项目均被处理。阻塞将持续到为队列中的每个项目均调用q.task_done()方法为止。 
下面的例子说明如何建立永远运行的进程,使用和处理队列上的项目。生产者将项目放入队列,并等待它们被处理。

JoinableQueue解决问题:

from multiprocessing import Process,JoinableQueue
import time,random,os
def consumer(q):
    while True:
        res=q.get()
        time.sleep(random.randint(1,3))
        print('\033[45m%s 吃 %s\033[0m' %(os.getpid(),res))
        q.task_done() #向q.join()发送一次信号,证明一个数据已经被取走了

def producer(name,q):
    for i in range(10):
        time.sleep(random.randint(1,3))
        res='%s%s' %(name,i)
        q.put(res)
        print('\033[44m%s 生产了 %s\033[0m' %(os.getpid(),res))
    q.join() #生产完毕,使用此方法进行阻塞,直到队列中所有项目均被处理。


if __name__ == '__main__':
    q=JoinableQueue()
    #生产者们:即厨师们
    p1=Process(target=producer,args=('包子',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

    #开始
    p_l=[p1,p2,p3,c1,c2]
    for p in p_l:
        p.start()

    p1.join()
    p2.join()
    p3.join()
    print('主') 
    
    #主进程等--->p1,p2,p3等---->c1,c2
    #p1,p2,p3结束了,证明c1,c2肯定全都收完了p1,p2,p3发到队列的数据
    #因而c1,c2也没有存在的价值了,不需要继续阻塞在进程中影响主进程了。应该随着主进程的结束而结束,所以设置成守护进程就可以了。

ps:我的理解是:生产者内部有一个计数器,每生产一个数据计数器+1,消费者每消费一个,通过task_done向生产者发送一个信号,这是计数器就-1,当计数器为0,即所有的数据都被处理后,生产者的join方法不再阻塞,生产者执行结束,主进程结束,消费者为守护进程,随着主进程结束而结束。

进程池和multiprocessing .Pool模块

进程池

当我们有成千上万个任务需要执行时,创建相应的成千上万个进程是不现实的。首先,创建/销毁进程需要时间;其次没操作系统也不能让让它们同时执行,反而影响效率。这里我们就需要一个进程池。
什么是进程池呢?
进程池,就是一个池子,在里面放上固定数量的进程,有需求来了,就拿一个池中的进程来处理任务,等到处理完毕,进程并不关闭,而是将进程再放回进程池中继续等待任务。如果有很多任务需要执行,池中的进程数量不够,任务就要等待之前的进程执行任务完毕归来,拿到空闲进程才能继续执行。也就是说,池中进程的数量是固定的,那么同一时间最多有固定数量的进程在运行。这样不会增加操作系统的调度难度,还节省了开闭进程的时间,也一定程度上能够实现并发效果。

multiprocessing.Pool

Pool([numprocess  [,initializer [, initargs]]]):创建进程池
1 numprocess:要创建的进程数,如果省略,将默认使用cpu_count()的值
2 initializer:是每个工作进程启动时要执行的可调用对象,默认为None
3 initargs:是要传给initializer的参数组

p = Pool()
#主要方法:
1 p.apply(func [, args [, kwargs]]):在一个池工作进程中执行func(*args,**kwargs),然后返回结果。
'''需要强调的是:此操作并不会在所有池工作进程中并执行func函数。如果要通过不同参数并发地执行func函数,必须从不同线程调用p.apply()函数或者使用p.apply_async()'''

2 p.apply_async(func [, args [, kwargs]]):在一个池工作进程中执行func(*args,**kwargs),然后返回结果。
 '''此方法的结果是AsyncResult类的实例,callback是可调用对象,接收输入参数。当func的结果变为可用时,将理解传递给callback。callback禁止执行任何阻塞操作,否则将接收其他异步操作中的结果。'''

3 p.close():关闭进程池,防止进一步操作。如果所有操作持续挂起,它们将在工作进程终止前完成
4 p.jion():等待所有工作进程退出。此方法只能在close()或teminate()之后调用

代码示例

#同步
import os,time
from multiprocessing import Pool

def work(n):
    print('%s run' %os.getpid())
    time.sleep(3)
    return n**2

if __name__ == '__main__':
    p=Pool(3) #进程池中从无到有创建三个进程,以后一直是这三个进程在执行任务
    res_l=[]
    for i in range(10):
        res=p.apply(work,args=(i,)) # 同步调用,直到本次任务执行完毕拿到res,等待任务work执行的过程中可能有阻塞也可能没有阻塞
                                    # 但不管该任务是否存在阻塞,同步调用都会在原地等着
    print(res_l)

#异步
import os
import time
import random
from multiprocessing import Pool

def work(n):
    print('%s run' %os.getpid())
    time.sleep(random.random())
    return n**2

if __name__ == '__main__':
    p=Pool(3) #进程池中从无到有创建三个进程,以后一直是这三个进程在执行任务
    res_l=[]
    for i in range(10):
        res=p.apply_async(work,args=(i,)) # 异步运行,根据进程池中有的进程数,每次最多3个子进程在异步执行
                                          # 返回结果之后,将结果放入列表,归还进程,之后再执行新的任务
                                          # 需要注意的是,进程池中的三个进程不会同时开启或者同时结束
                                          # 而是执行完一个就释放一个进程,这个进程就去接收新的任务。  
        res_l.append(res)

    # 异步apply_async用法:如果使用异步提交的任务,主进程需要使用jion,等待进程池内任务都处理完,然后可以用get收集结果
    # 否则,主进程结束,进程池可能还没来得及执行,也就跟着一起结束了
    p.close()
    p.join()
    for res in res_l:
        print(res.get()) #使用get来获取apply_aync的结果,如果是apply,则没有get方法,因为apply是同步执行,立刻获取结果,也根本无需get

回调函数

import os
from multiprocessing import Pool

def func(n):
	print('子进程:', os.getpid())
	print('这里是子进程函数:', n)
	return n

def call_back_func(a):
	print('回调函数:', os.getpid())
	print('这里是回调函数:', a)


if __name__ == '__main__':
	p = Pool()
	p.apply_async(func=func, args=('test',), callback=call_back_func)

	p.close()
	p.join()
	print('主函数:', os.getpid())

执行结果:
在这里插入图片描述
可以看到,回到函数的参数是子进程函数的返回值。这就要求如果有回调函数,子进程函数需要有返回值,且有几个返回值回调函数就要有几个参数。并且回调函数是在主进程中执行的,并不是子进程中。注意注意!!!!

发布了32 篇原创文章 · 获赞 32 · 访问量 6816

猜你喜欢

转载自blog.csdn.net/qq_33267875/article/details/96761427