Python并发编程之进程间通信

一、Python进程间通信

multiprocessing模块支持进程间通信的两种主要形式:管道和队列。这两种方法都是实用消息传递来实现的,但队列接口有意模仿线程程序中参见的队列用法。

二、使用队列Queue进行进程间通信

Queue([maxsize])   

创建共享的进程队列。maxsize是队列中允许的最大项数,如果忽略此参数,则无大小限制。底层队列使用管道和锁定实现。

2.1 Queue实例中的方法和属性

Queue的实例具有以下方法

  • q.cancel_join_thread()
  • q.close()
  • q.empty()
  • q.full()
  • q.get([block [, timeout]])
  • q.get_nowait()
  • q.join_thread()
  • q.put(item [, block [, timeout]])
  • q.put_nowait(item)
  • q.qsize()
JoinableQueue([maxsize])   

创建可连接的共享进程队列。这就像是一个Queue对象,但队列允许项目的使用者通知生产者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。
JoinableQueue的实例p除了与Queue对象相同的方法外,还具有以下方法:

  • q.task_done()
    消费者使用此方法发出信号,表示q.get()返回的项目已经被处理完毕。如果调用此方法的次数大于队列中删除的项目数量,将引发ValueError异常
  • q.join()
    生产者使用此方法进行阻塞,直到队列中的所有项目都被处理。阻塞将持续到位队列中的每个项目均调用q.task_done()方法为止。

2.2 使用Queue进行进程间通信的代码示例

import multiprocessing

def consumer(input_q):
    while True:
        item = input_q.get()
        # 进行处理工作,此处替换为实际有意义的工作
        print(item)
        # 发出信号通知任务完成
        input_q.task_done()

def producer(sequence, output_q):
    for item in sequence:
        # 将项目放入队列中
        output_q.put(item)

# 建立进程
if __name__ == '__main__':
    q = multiprocessing.JoinableQueue()
    # 运行消费者进程
    consumer_p = multiprocessing.Process(target=consumer, args=(q, ))
    consumer_p.daemon = True
    consumer_p.start()

    # 生产项目,sequence代表要发送给消费者的项目序列
    # 在实践中,这可能是生成器的输出或通过一些其他方式生产出来
    sequence = [1, 2, 3, 4, 5]
    producer(sequence, q)

    # 等待所有项目被处理
    q.join()

这个例子中,将消费者进程设置为后台进程,因为它永久运行,而我们希望但主程序结束时它随之终止(如果忘记这么做,程序将挂起)。这里使用了JoinableQueue,以便让生产者实际了解队列中的所有项目何时被处理完毕。join()操作保证了这一点。如果忘记这个步骤,消费者进程将在有时间完成所有工作之前被终止。

如果需要,可以在同一个队列中放置多个进程,也可以从同一个队列中获取多个进程,例如,如果要构造消费者进程池,可以编写下面这样的代码:

if __name__ == "__main__":
    q = multiprocessing.JoinableQueue()

    # 启动一些消费者进程
    consumer_p1 = multiprocessing.Process(target=consumer, args=(q, ))
    consumer_p1.daemon = True
    consumer_p1.start()

    consumer_p2 = multiprocessing.Process(target=consumer, args=(q, ))
    consumer_p2.daemon = True
    consumer_p2.start()

    # 生产项目
    sequence = [1, 2, 3, 4, 5]
    producer(sequence, q)

    # 等待所有项目被处理
    q.join()

编写这类代码时,要把队列中的每个项目都序列化,然后通过管道或套接字连接发送给进程。一般的规则是:发送数据量较少的大对象比发送大量小对象要好。

在某些应用程序中,生产者需要通知消费者,它们不在生产任何项目而应该关闭。为此,编写的代码中应该使用标志(sentinel 指示完成的特殊值)。下面这个例子使用none作为标志说明这个概念:

import multiprocessing

def consumer(input_q):
    while True:
        item = input_q.get()
        if item is None:
            break
        # 处理项目
        print(item)
    # 关闭
    print("consumer done")

def producer(sequence, output_q):
    for item in sequence:
        # 把项目放入队列
        output_q.put(item)

if __name__ == '__main__':
    q = multiprocessing.JoinableQueue()

    # 启动消费者进程
    consumer_p = multiprocessing.Process(target=consumer, args=(q, ))
    consumer_p.daemon = True
    consumer_p.start()

    # 生产项目
    sequence = [1, 2, 3, 4, 5]
    producer(sequence, q)

    # 在队列中放置标志,发出完成信号
    q.put(None)

    # 等待所有项目被处理, 然后关闭
    q.join()

如果像上面这个例子中那样使用标志,一定要在队列上为每个使用者都放上标志。例如,如果有三个消费者进程在使用队列上的项目,那么生产者需要在队列中放置三个标志,才能让所有消费者都关闭。

三、使用管道Pipe进行进程间通信

作为使用队列的另一种形式,还可以使用管道在进程之间执行消息传递。

Pipe([duplex])  

在进程之间创建一条管道,并返回元组(conn1, conn2), 其中conn1和conn2是表示管道两端的Connection对象。默认情况下,管道是双向的。如果将duplex设置为false,则conn1只能用于接收,conn2只能用于发送。必须在创建和启动使用管道的Process对象之前调用Pipe()方法。

3.1 Pipe实例的方法和属性

Pipe()方法返回的Connection对象的实例c具有以下方法和属性

  • c.close()
  • c.fileno()
  • c.poll([timeout])
  • c.recv()
  • c.recv_bytes(buffer [, offset [, size]])

3.2 使用Pipe进行进程间通信的代码示例

可以使用与队列类似的方式使用管道。下面这个例子说明如何使用管道实现前面的生产者消费者问题:

import multiprocessing

# 使用管道上的项目
def consumer(pipe):
    output_p, input_p = pipe
    # 关闭管道的输入端
    input_p.close()

    while True:
        try:
            item = output_p.recv()
        except EOFError:
            # 如果连接的另一端已经关闭,再也不存在任何数据,将引发EOFError异常
            break
        # 处理项目
        print(item)
    # 关闭
    print("consumer done")

# 生产项目并将其放到队列上
def producer(sequence, input_p):
    for item in sequence:
        input_p.send(item)

if __name__ == "__main__":
    (output_p, input_p) = multiprocessing.Pipe()
    # 启动消费者进程
    consumer_p = multiprocessing.Process(target=consumer, args=((output_p, input_p), ))
    consumer_p.start()

    # 关闭生产者中的输出管道
    output_p.close()
    # 生产项目
    sequence = [1, 2, 3, 4, 5]
    producer(sequence, input_p)

    # 关闭输入管道,表示完成
    input_p.close()

    # 等待消费者进程关闭
    consumer_p.join()

应当特别注意管道端口的正确管理。如果生产者或消费者中都没有使用管道的某个端点,就应将其关闭。这也说明了为何在生产者中关闭了管道的输出端,在消费者中关闭了管道的输入端。如果忘记执行这些步骤,程序可能会在消费者中的recv()操作上挂起。

管道是由操作系统进行引用计数的,必须在所有进程中关闭管道后才能生成EOFError异常。因此,在生产者中关闭管道不会有任何效果,除非消费者也关闭了相同的管道端点。

管道可用于双向通信。利用通常在客户端/服务器计算中使用的请求/响应模型或远程过程调用,就可以使用管道编写与进程交互的程序,例如:

import multiprocessing

# 服务器处理
def adder(pipe):
    server_p, client_p = pipe
    # 关闭服务器中的客户端管道
    client_p.close()
    while True:
        try:
            x, y = server_p.recv()
        except EOFError:
            break
        result = x + y
        server_p.send(result)

    # 关闭
    print("server done")

if __name__ == "__main__":
    (server_p, client_p) = multiprocessing.Pipe()

    # 启动服务器进程
    adder_p = multiprocessing.Process(target=adder, args=((server_p, client_p), ))
    adder_p.start()

    # 关闭客户端中的服务器管道
    server_p.close()

    # 向服务器提出一些请求
    client_p.send((3, 4))
    print(client_p.recv())

    client_p.send(("hello", "world"))
    print(client_p.recv())

    # 完成, 关闭管道
    client_p.close()

    # 等待消费者进程关闭
    adder_p.join()

这个例子中,adder()函数以服务器的形式运行,等待消息到达管道的端点。收到之后,它会执行一些处理并将结果返回给管道。send()和recv()方法使用pickle模块对对象进行序列化。但对于使用远程过程调用的高级应用程序而言,应该使用进程池。

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

smart_cat

你的鼓励将是我写作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值