Day34 of learning python --进程的进阶

1.进程间通信--队列和管道(multiprocess.Queue、multiprocess.Pipe)

  进程间的通信称为IPC(inter-Process Communication),这两种方法都是基于消息传递实现的,

(1)队列

  创建共享的进程队列,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()方法)
Queue的方法
q.close() 
关闭队列,防止队列中加入更多数据。调用此方法时,后台线程将继续写入那些已入队列但尚未写入的数据,但将在此方法完成时马上关闭。如果q被垃圾收集,将自动调用此方法。关闭队列不会在队列使用者中生成任何类型的数据结束信号或异常。例如,如果某个使用者正被阻塞在get()操作上,关闭生产者中的队列不会导致get()方法返回错误。

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

q.join_thread() 
连接队列的后台线程。此方法用于在调用q.close()方法后,等待所有队列项被消耗。默认情况下,此方法由不是q的原始创建者的所有进程调用。调用q.cancel_join_thread()方法可以禁止这种行为。
Queue不常用的方法

 队列的方法实现代码:

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()) #空了
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  Queue,Process
def produce(q):
    q.put('hello')

def consume(q):
    print(q.get())

if __name__=='__main__':
    q = Queue()
    p = Process(target=produce,args=(q,))
    p.start()
    c = Process(target=consume, args=(q,))
    c.start()

# 通过队列,不同进程之间可以交互数据
队列先进先出
from multiprocessing import Process,Queue
import time,os
def InpQ(queue):
    info = str(os.getpid())+'(put):'+str(time.asctime())
    print(info)
    queue.put(info)
def OutQ(queue):
    info = queue.get()
    print('%s%s\033[32m%s\033[0m'%(str(os.getpid()),'(get):',info))

if __name__ == '__main__':
    queue = Queue()
    inp = []
    oup = []
    for i in range(10):
        p = Process(target=InpQ,args=(queue,))
        p.start()
        inp.append(p)
    for i in range(10):
        p = Process(target=OutQ,args=(queue,))
        p.start()
        oup.append(p)
    for i in inp:
        i.join()
    for i in oup:
        i.join()
复杂一点的队列应用

(2)生产者消费者模型

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

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

基于队列实现生产者消费者模型

初始版本

所以解决了不冲突的问题,但是,此时,主程序永远不会结束,因为是生产者生产完成之后就结束了,但是消费者在取空了队列里面的数据之后,则一直处于死循环并且卡在q.get()这一步。

解决的办法是:让生产者在生产完毕后,往队列中再发一个结束信号,这样消费者在接收到结束信号后就可以break出死循环了。

from multiprocessing import Process,Queue
import os,random,time
def Produce(queue):
    for i in range(10):
        res = '包子%s'%i
        time.sleep(random.randint(1,3))
        queue.put(res)
        print('\033[44m%s生产了%s\033[0m'%(os.getpid(),res))
    queue.put(None)

def Consumer(queue):
    while 1:
        time.sleep(random.randint(5,7))
        res = queue.get()
        if res == None:break
        print('\033[31m%s吃了%s\033[0m'%(os.getpid(),res))

if __name__ == '__main__':
    queue = Queue()
    prod = Process(target=Produce,args=(queue,))
    consumer = Process(target=Consumer,args=(queue,))
    prod.start()
    consumer.start()
    print('主程序代码结束了')
改良版本

注意:结束信号None,不一定要由生产者发,主进程里同样可以发,但是主进程需要等生产者结束后才应该发送该信号

同时,在有多个生产者和消费者时,我们 则需要用一个很low的方式去解决

from multiprocessing import Process,Queue
import os,random,time
def Produce(name,queue):
    for i in range(10):
        res = name+'%s'%i
        time.sleep(random.randint(1,3))
        queue.put(res)
        print('\033[44m%s生产了%s\033[0m'%(os.getpid(),res))

def Consumer(queue):
    while 1:
        time.sleep(random.randint(5,7))
        res = queue.get()
        if res == None:break
        print('\033[31m%s吃了%s\033[0m'%(os.getpid(),res))

if __name__ == '__main__':
    queue = Queue()
    prod = Process(target=Produce,args=('泔水',queue,))
    prod2 = Process(target=Produce, args=('包子', queue,))
    prod3 = Process(target=Produce, args=('肠粉', queue,))

    consumer = Process(target=Consumer,args=(queue,))
    consumer2 = Process(target=Consumer, args=(queue,))

    prod.start()
    prod2.start()
    prod3.start()
    consumer.start()
    consumer2.start()
    prod.join()  # 必须等到全部生产完成之后,才能发结束信号
    prod2.join()
    prod3.join()

    queue.put(None)  #有几个消费者,就发送多个结束信号
    queue.put(None)
    print('主程序代码结束了')
多个生产者和消费者

JoinableQueue([maxsize])

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

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

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

q.join() 
生产者将使用此方法进行阻塞,直到队列中所有项目均被处理。阻塞将持续到为队列中的每个项目均调用q.task_done()方法为止。 
下面的例子说明如何建立永远运行的进程,使用和处理队列上的项目。生产者将项目放入队列,并等待它们被处理。
from multiprocessing import Process,JoinableQueue
import os,random,time
def Produce(name,queue):
    for i in range(10):
        res = name+'%s'%i
        time.sleep(random.randint(1,3))
        queue.put(res)
        print('\033[44m%s生产了%s\033[0m'%(os.getpid(),res))
    queue.join()  # 生产完毕,使用此方法进程阻塞,直到队列中所有项目均被处理。

def Consumer(queue):
    while 1:
        time.sleep(random.randint(5,7))
        res = queue.get()
        print('\033[31m%s吃了%s\033[0m'%(os.getpid(),res))
        queue.task_done()  # 向q.join()发送一次信号,证明一个数据已经被取走了

if __name__ == '__main__':
    queue = JoinableQueue()
    prod = Process(target=Produce,args=('泔水',queue,))
    prod2 = Process(target=Produce, args=('包子', queue,))
    prod3 = Process(target=Produce, args=('肠粉', queue,))

    consumer = Process(target=Consumer,args=(queue,))
    consumer2 = Process(target=Consumer, args=(queue,))
    consumer.daemon = True
    consumer2.daemon = True

    p_l = [prod, prod2, prod3,consumer, consumer2]
    for p in p_l:
        p.start()
    prod.join()  # 必须等到全部生产完成之后,才能发结束信号
    prod2.join()
    prod3.join()

    print('主程序代码结束了')
JoinableQueue队列实现消费之生产者模型

主进程等待-->prod,prod2,prod3结束-->consumer,consumer2结束后。多个进程并发执行的。prod,prod2,prod3结束了,证明consumer,consumer2肯定是取完了prod,prod2,prod3发到队列中的数据。

所有consumer,consumer2也就没有存在的价值了,不需要继续阻塞在进程中影响主进程了,应该随着主进程的结束而结束,所有设置了守护进程就可以了。

 2.进程之间的数据共享

  基于消息传递的并发编程是大势所在,即便是使用线程,推荐的做法也是将程序设计为大量独立的线程集合,通过消息队列交换数据。这样极大地减少了对使用锁定和其他同步手段地需求,还可以扩展到分布式系统中。

但进程间应该尽量避免通信,即便需要通信,也应该选择进程安全的工具来避免加锁带来的问题。

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

方法一:
from multiprocessing import Manager,Process,Lock

def main(dic,lock):
    lock.acquire()
    dic['count'] -= 1
    # print(dic)
    lock.release()
if __name__ == '__main__':
    m = Manager()
    l = Lock()
    dic = m.dict({'count':100})
    p_lst = []
    # p = Process(target=main, args=(dic,))
    # p.start()
    # p.join()
    # print('主进程:',dic)
    for i in range(50):
        p = Process(target=main,args=(dic,l))
        p.start()
        p_lst.append(p)
    for i in p_lst:
        i.join()
    print('主进程:',dic)

# 使用Manager也存在数据安全,因此也需要加锁

方法二:
from multiprocessing import Manager,Process,Lock
def work(d,lock):
    with lock: #不加锁而操作共享的数据,肯定会出现数据错乱
        d['count']-=1

if __name__ == '__main__':
    lock=Lock()
    with Manager() as m:
        dic=m.dict({'count':100})
        p_l=[]
        for i in range(100):
            p=Process(target=work,args=(dic,lock))
            p_l.append(p)
            p.start()
        for p in p_l:
            p.join()
        print(dic)
Manager的使用

 3.进程池和multiprocess.Pool模块

(1)进程池的概念

  在程序实际处理问题过程中,开启成千上万的进程的活,由于创建进程需要消耗时间,销毁进程也需要时间,而且操作系统也不能让他们同时执行,这样反而会影响程序的效率。

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

(2)multiprocess.Pool模块

Pool([numprocess  [,initializer [, initargs]]]):创建进程池
1 numprocess:要创建的进程数,如果省略,将默认使用cpu_count()的值
2 initializer:是每个工作进程启动时要执行的可调用对象,默认为None
3 initargs:是要传给initializer的参数组
参数介绍
p.apply(func [, args [, kwargs]]):在一个池工作进程中同步执行func(*args,**kwargs),等待返回结果,然后再执行下一个。
p.apply_async(func [, args [, kwargs]]):在一个池工作进程中异步执行func(*args,**kwargs),然后返回结果。
 p.close():关闭进程池,防止进一步操作。如果所有操作持续挂起,它们将在工作进程终止前完成
P.jion():等待所有工作进程退出。此方法只能在close()或teminate()之后调用
常用方法使用
方法apply_async()和map_async()的返回值是AsyncResul的实例obj。实例具有以下方法
obj.get():返回结果,如果有必要则等待结果到达。timeout是可选的。如果在指定时间内还没有到达,将引发一场。如果远程操作中引发了异常,它将在调用此方法时再次被引发。
obj.ready():如果调用完成,返回True
obj.successful():如果调用完成且没有引发异常,返回True,如果在结果就绪之前调用此方法,引发异常
obj.wait([timeout]):等待结果变为可用。
obj.terminate():立即终止所有工作进程,同时不执行任何清理或结束任何挂起工作。如果p被垃圾回收,将自动调用此函数
其他方法使用

 进程池和多进程效率对比

   进程池的效率,比多进程效率要高

from multiprocessing import Manager,Process,Lock,Pool
def work():
    print('执行啦')
if __name__ == '__main__':
    p = Pool(5,work)
    print('主程序运行完了')

#主进程没有等子程序运行完,就结束了
from multiprocessing import Pool,Process
import time
def func(n):
    for i in range(10):
        print(n+1)

if __name__=='__main__':
    s1 = time.time()
    pool = Pool(5)    # 5个进程
    pool.map(func,range(100))  # 100个任务,map是异步执行的
    t1 = time.time() - s1
    s2 = time.time()
    print('主程序执行完了')
    p_lst = []
    for i in range(100):
        p = Process(target=func,args=(i,))
        p_lst.append(p)
        p.start()
    for p in p_lst:p.join()
    t2 = time.time() -s2
    print(t1,t2)
结果:
0.4150102138519287 10.365068435668945
对比测试

 同步和异步

from multiprocessing import Manager,Process,Lock,Pool
import os
import time
def work(n):
    print('%srun'%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执行的过程中可能有阻塞也可能没有阻塞,但是同步调用都会在原地等待
        res_l.append(res)
    print(res_l)
    print('主程序运行完了')
apply()同步调用
import os
import time
import random
from multiprocessing import Pool

def work(n):
    print('%s run' %n)
    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
    print('主进程已经结束了')
apply_async()异步调用,主进程结束,也随着结束,需要用到close()和join()

 使用进程池来实现并发socket效果

from multiprocessing  import Pool
import socket,os
# Pool内的进程数默认是cpu核数
def talk(conn):
    conn.send(b'hello')
    while 1:
        try:
            ret = conn.recv(1024)
            if ret == 'q':
                break
            print('进程pid:%s,%s'%(os.getpid(),ret.decode('utf-8')))
            # info = input('>>>').strip()
            conn.send('我不能说我想说的话,因为子进程不支持客户端说话说话'.encode('utf-8'))
        except Exception:
            break
    conn.close()

if __name__ == '__main__':
    pool = Pool(5)  #
    # print(os.cpu_count())  # 查看CPU的个数
    sk = socket.socket()
    sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    sk.bind(('127.0.0.1',8090))
    sk.listen()
    while 1:
        conn,addr = sk.accept()
        pool.apply_async(talk,args=(conn,))
        # pool.apply(talk, args=(conn,))  # 同步的话,则同一时间只能有一个客户端能访问
server端
from socket import *
sk = socket()
sk.connect(('127.0.0.1',8090))

msg = sk.recv(1024)
print(msg.decode('utf-8'))

while 1:
    info = input('>>>').strip()
    sk.send(info.encode('utf-8'))
    if info == 'q':
        break
    msg = sk.recv(1024)
    print(msg.decode('utf-8'))
client端

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

回调函数

回调函数的定义
回调函数的应用场景:进程池中任何一个任务一旦处理完成,就立即告知主进程:你可以处理我的结果了。主进程则调用一个函数去处理结果,该函数即回调函数。

因为我们可以把耗时间的任务放到进程池中,然后指定回调函数(主进程负责执行),这样主进程在执行回调函数时就省去了I/0的过程,直接拿到任务的结果。

# 回调函数
from multiprocessing import Pool
import os
def func1(n):
    print('in func1',os.getpid())
    return n*n

def func2(nn):
    print('in func2',os.getpid(),nn)


if __name__ =='__main__':
    p = Pool(5)
    for i in range(10):
        p.apply_async(func1,args=(i,),callback=func2)
    print('主进程',os.getpid())
    p.close()
    p.join()
    print('代码结束')
回调函数的使用

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

from multiprocessing import Pool
import time,random,os

def work(n):
    time.sleep(1)
    print('执行完进程%s'%n)
    return n**2

if __name__ == '__main__':
    p=Pool(5)

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

    p.close()
    print('关闭了进程池')
    p.join() #等待进程池中所有进程执行完毕
    print('执行了主进程后的代码')

    nums=[]
    for res in res_l:
        nums.append(res.get()) #拿到所有结果
    print(nums) #主进程拿到所有的处理结果,可以在主进程中进行统一进行处理
用get()函数取得进程的返回值
# 回调函数是在主进程中执行的,用在爬虫的情况,由于访问网页网络延时很长,所以交给子进程来做,处理数据交给主进程
import requests
from multiprocessing import Pool
from urllib.request import urlopen
# 404(状态码)网页找不到
# 200,网页正常返回
def get(url):
    response = requests.get(url)
    if response.status_code == 200:
        return url,response.content.decode('utf-8')
# def get_urllib(url):
#     ret = urlopen(url)
#     return ret.read().decode('utf-8')

def call_back(args):
    url,content = args
    print(url,len(content))

if __name__ =='__main__':
    url_lst = [
        'https://www.cnblogs.com/',
        'http://www.baidu.com/',
        'https://www.taobao.com/',
        'https://www.sogou.com/',
        'http://www.sohu.com'
    ]
    p = Pool(5)
    for url in url_lst:
        p.apply_async(get,args=(url,),callback=call_back)
    p.close()
    p.join()
小小爬虫
import re
from urllib.request import urlopen
from multiprocessing import Pool

def get_page(url,pattern):
    response = urlopen(url).read().decode('utf-8')
    return pattern,response

def parse_page(info):
    pattern,page_content = info
    res = re.findall(pattern,page_content)
    for item in res:
        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>'
    pattern1 = re.compile(regex,re.S)
    url_dic = {'http://maoyao.com/board/7':pattern1}
    p = Pool()
    res_l = []
    for url,pattern in url_dic.items():
        res = p.apply_async(get_page,args=(url,pattern),callback=parse_page)
        res_l.append(res)

    p.close()
    p.join()
    for i in res_l:
        print(i.get())
爬取电影排名
posted on 2018-12-14 18:52  smile大豆芽  阅读(149)  评论(0)    收藏  举报