《Python》进程之间的通信(IPC)进程之间的数据共享进程池

Posted zyling_me

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Python》进程之间的通信(IPC)进程之间的数据共享进程池相关的知识,希望对你有一定的参考价值。

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

  进程间通信:IPC(inter-Process Communication)

1、队列

概念介绍:

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

技术图片
# 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()方法可以禁止这种行为。
# Queue([maxsize]) 
创建共享的进程队列。
参数 :maxsize是队列中允许的最大项数。如果省略此参数,则无大小限制。



底层队列使用管道和锁定实现。
代码实现:

代码实现

‘‘‘
multiprocessing模块支持进程间通信的两种主要形式:管道和队列 都是基于消息传递实现的,但是队列接口 ‘‘‘ from multiprocessing import Queue q = Queue(3) # 只能往这个队列放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()) # 判断是否空了
from multiprocessing import Process, Queue

def consume(q):
    print(son-->, q.get())    # 取走队列一条数据
    q.put(abc)    # 给队列增加一条数据

if __name__ == __main__:
    q = Queue()
    p = Process(target=consume, args=(q,))
    p.start()
    q.put({haha: 123})    # 给队列增加一条数据
    p.join()    # 等待子进程执行完毕
    print(Foo-->, q.get())    # 取走队列一条数据
# son--> {‘haha‘: 123}
# Foo--> abc 

2、生产者消费者模型:

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

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

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

什么是生产者消费者模式

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

import time
import random
from multiprocessing import Process, Queue

def consumer(q, name):
    # 处理数据
    while 1:
        food = q.get()
        if food is None: break  # 如果不结束的话程序就会一直不结束
        time.sleep(random.uniform(0.5, 1))
        print(%s吃了一个%s % (name, food))

def producer(q, name, food):
    # 获取数据
    for i in range(10):
        time.sleep(random.uniform(0.3, 0.8))
        print(%s生产了%s%s % (name, food, i + 1))
        q.put(food + str(i))

if __name__ == __main__:
    q = Queue()
    Process(target=consumer, args=(q, alex)).start()
    Process(target=consumer, args=(q, wusir)).start()
    p1 = Process(target=producer, args=(q, yang, 包子))
    p2 = Process(target=producer, args=(q, sihao, 馒头))
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    q.put(None)    # 有几个consumer就需要放几个None
    q.put(None)    # 结束信号 

3、JoinableQueue([maxsize])

  创建可连接的共享进程队列。这就像是一个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 consumer(q, name):
    # 处理数据
    while 1:
        food = q.get()
        time.sleep(random.uniform(0.5, 1))
        print(%s吃了一个%s % (name, food))
        q.task_done()   # 通知队列已经有一个数据被处理了
        
def producer(q, name, food):
    # 获取数据
    for i in range(10):
        time.sleep(random.uniform(0.3, 0.8))
        print(%s生产了%s%s % (name, food, i+1))
        q.put(food + str(i))
        
if __name__ == __main__:
    q = JoinableQueue()
    c1 = Process(target=consumer, args=(q, alex))
    c2 = Process(target=consumer, args=(q, wusir))
    c1.daemon = True    # 设置守护进程
    c2.daemon = True    # 设置守护进程
    c1.start()
    c2.start()
    p1 = Process(target=producer, args=(q, yang, 包子))
    p2 = Process(target=producer, args=(q, sihao, 馒头))
    p1.start()
    p2.start()
    p1.join()   # 生产者要先把所有的数据都放到队列中
    p2.join()   # 生产者要先把所有的数据都放到队列中
    q.join()    # 阻塞直到放入队列中所有的数据都被处理掉(有多少个数据就接收到了多少task_done)

4、管道

  队列是基于管道实现的,管道是基于 socket 实现的

  队列 + 锁  简便的IPC机制,使得进程之间数据安全

  管道  进程之间数据不安全,且存取数据复杂

  socket + pickle

 

技术图片
#创建管道的类:
Pipe([duplex]):在进程之间创建一条管道,并返回元组(conn1,conn2),其中conn1,conn2表示管道两端的连接对象,强调一点:必须在产生Process对象之前产生管道
#参数介绍:
dumplex:默认管道是全双工的,如果将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异常。
介绍

 

import time
from multiprocessing import Pipe, Process

def consumer(left, right):
    time.sleep(1)
    print(right.recv())

if __name__ == __main__:
    left, right = Pipe()
    Process(target=consumer, args=(left, right)).start()
    left.send(1234)

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

from multiprocessing import Process, Pipe

def consumer(left, right):
    left.close()    # 不写close将不会引发EOFError
    while 1:
        try:
            print(right.recv())
        except EOFError:
            break

if __name__ == __main__:
    left, right = Pipe()
    Process(target=consumer, args=(left, right)).start()
    right.close()
    for i in range(10):
        left.send(包子%s % i)
    left.close()
# pipe的端口管理不会随着某一个进程的关闭就关闭
# 操作系统来管理进程对这些端口的使用
# left,right
# left,right
# 操作系统管理4个端口  每关闭一个端口计数-1,直到所有的端口都关闭了
# 剩余1个端口的时候 recv就会报错

 二、进程之间的数据共享   

  展望未来,基于消息传递的并发编程是大势所趋

  即便是使用线程,推荐做法也是将程序设计为大量独立的线程集合,通过消息队列交换数据。

  这样极大地减少了对使用锁定和其他同步手段的需求,还可以扩展到分布式系统中。

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

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

技术图片
进程间数据是独立的,可以借助于队列或管道实现通信,二者都是基于消息传递的
虽然进程间数据独立,但可以通过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 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)

三、数据池和multiprocess.Pool模块

 进程池:

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

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

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

 multiprocess.Pool 模块

 概念介绍:

Pool([numprocess  [,initializer [, initargs]]]):创建进程池

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

# 主要方法:
1 p.apply(func [, args [, kwargs]]):在一个池工作进程中执行func(*args,**kwargs),然后返回结果。
2 ‘‘‘需要强调的是:此操作并不会在所有池工作进程中并执行func函数。如果要通过不同参数并发地执行func函数,
必须从不同线程调用p.apply()函数或者使用p.apply_async()‘‘‘
3 p.apply_async(func [, args [, kwargs]]):在一个池工作进程中执行func(*args,**kwargs),然后返回结果。
4 ‘‘‘此方法的结果是AsyncResult类的实例,callback是可调用对象,接收输入参数。当func的结果变为可用时,
将理解传递给callback。callback禁止执行任何阻塞操作,否则将接收其他异步操作中的结果。‘‘‘   
5 p.close():关闭进程池,防止进一步操作。如果所有操作持续挂起,它们将在工作进程终止前完成
6 P.jion():等待所有工作进程退出。此方法只能在close()或teminate()之后调用

# 其他方法(了解)
1 方法apply_async()和map_async()的返回值是AsyncResul的实例obj。实例具有以下方法
2 obj.get():返回结果,如果有必要则等待结果到达。timeout是可选的。如果在指定时间内还没有到达,将引发一场。
如果远程操作中引发了异常,它将在调用此方法时再次被引发。
3 obj.ready():如果调用完成,返回True
4 obj.successful():如果调用完成且没有引发异常,返回True,如果在结果就绪之前调用此方法,引发异常
5 obj.wait([timeout]):等待结果变为可用。
6 obj.terminate():立即终止所有工作进程,同时不执行任何清理或结束任何挂起工作。如果p被垃圾回收,将自动调用此函数

 代码实例:

   同步:

# 进程池的 同步调用  apply

import os
import time
from multiprocessing import Pool

def task(num):
    time.sleep(1)
    print(%s : %s % (num, os.getpid()))
    return num**2

if __name__ == __main__:
    p = Pool()
    for i in range(20):
        res = p.apply(task, args=(i,))  # 提交任务的方法,同步提交
        print(-->, res)

  异步:

# 进程池的 异步调用  apply_async

import os
import time
from multiprocessing import Pool
# 没有取返回值
def task(num):
    time.sleep(1)
    print(%s : %s % (num, os.getpid()))
    return num**2

if __name__ == __main__:
    p = Pool()
    for i in range(20):
        p.apply_async(task, args=(i,))  # 提交任务的方法,异步提交
    p.close()
    p.join()

# 通过队列取返回值
def task(num):
    time.sleep(1)
    print(%s : %s % (num, os.getpid()))
    return num**2

if __name__ == __main__:
    p = Pool()
    res_lst = []
    for i in range(20):
        res = p.apply_async(task, args=(i,))    # 提交任务的方法,异步提交
        res_lst.append(res)
    for res in res_lst:
        print(res.get())

   map() 方法:

import os
import time
from multiprocessing import Pool

def task(num):
    time.sleep(1)
    print(%s : %s % (num, os.getpid()))
    return num**2

if __name__ == __main__:
    p = Pool()
    p.map(task, range(20))

 

以上是关于《Python》进程之间的通信(IPC)进程之间的数据共享进程池的主要内容,如果未能解决你的问题,请参考以下文章

Python学习第20篇:互斥锁以及进程之间的三种通信方式(IPC)以及生产者个消费者模型

112 python程序中的进程操作-进程之间进行通信(mulitiProcessing Queue队列)

进程和线程之间的通信

多个相同进程之间的 IPC 通信

python全栈开发基础第二十一篇互斥锁以及进程之间的三种通信方式(IPC)以及生产者个消费者模型

IPC进程之间通信的几种方式