Python中的进程间通信——管道、队列和共享内存

290 阅读4分钟

管道

  • 管道通信是基于文件描述符的,因此它的效率相对较高。在大多数情况下,管道的通信速度与数据量和数据类型的关系不大。它的传输速度取决于内存或磁盘I/O的上限。

  • 管道是最简单的进程间通信方式之一,它是Unix系统中的一种进程间通信方法,因此在Unix和类Unix系统(如Linux)中常用(windows系统也可通过os.pipe()来使用管道通信)

  • 管道只支持单向通信:一个进程只能将数据写入管道,另一个进程从管道读取数据。如果需要双向通信,就需要创建两个管道。

  • 管道通常用于父子进程之间的通信,其中一个进程(通常是子进程)需要接收并处理来自父进程的数据。

  • 安全性:如果两个进程(或线程)同时尝试读取或写入管道的同一端会损坏。当然,在不同进程中同时使用管道的不同端的情况下不存在损坏的风险。

# linux环境下管道实现
from multiprocessing import Process, Pipe

def f(conn):
    conn.send([42, None, 'hello'])
    conn.close()

if __name__ == '__main__':
    # 子进程向父进程发送消息
    parent_conn, child_conn = Pipe()
    p = Process(target=f, args=(child_conn,))
    p.start()
    print(parent_conn.recv())   # prints "[42, None, 'hello']"
    p.join()

队列

  • 队列是一种更复杂的进程间通信机制,它允许进程之间进行双向通信。

  • 队列有并发访问控制机制,支持多生产者和多消费者的并发访问。

  • 队列通信通常比管道通信更复杂,因为它需要更多的同步和数据复制操作。如果多个进程或线程同时访问队列,那么队列的通信效率可能会受到影响。

  • 当一个对象被放入一个队列中时,这个对象首先会被一个后台线程用 pickle 序列化(在进程间传递图片时序列化和反序列化会带来较多CPU消耗),并将序列化后的数据通过一个底层管道的管道传递到队列中。

from multiprocessing import Process, Queue

def f(q):
    q.put([42, None, 'hello'])

if __name__ == '__main__':
    q = Queue()
    p = Process(target=f, args=(q,))
    p.start()
    print(q.get())    # prints "[42, None, 'hello']"
    p.join()

共享内存

  • 使用多进程时,一般使用消息机制实现进程间通信,尽可能避免使用同步原语,例如锁。而不使用锁的情况下,来自于多进程的输出很容易产生混淆,因此在进行并发编程时,通常最好尽量避免使用共享状态,尤其在使用多个进程时。

  • 这种类型的的共享内存允许不同进程读写一片公共(或者共享)的易失性存储区域。一般来说,进程被限制只能访问属于自己进程空间的内存,但是共享内存允许跨进程共享数据,从而避免通过进程间发送消息的形式传递数据。与通过磁盘、套接字或者其他要求序列化、反序列化和复制数据的共享形式相比,直接通过内存共享数据拥有更出色的性能。

  • 可以使用 Value 或 Array 将数据存储在共享内存映射中

from multiprocessing import Process, Value, Array

def f(n, a):
    n.value = 3.1415927
    for i in range(len(a)):
        a[i] = -a[i]

if __name__ == '__main__':
    num = Value('d', 0.0)
    arr = Array('i', range(10))

    p = Process(target=f, args=(num, arr))
    p.start()
    p.join()

    print(num.value)
    print(arr[:])

三种方式对比

管道队列共享内存
通信方式单向通信并发访问需加锁控制数据
是否需要序列化基础数据类型不需要,复杂数据类型需要基础数据类型不需要,复杂数据类型需要不需要
通信速度基于文件描述符速度较快数据量较小时速度较快数据可以省去序列化步骤从而有更快的通信速度
应用场景进程间数据交互关系较为单一数据量较小但数据交互关系较为复杂序列化操作复杂且数据修改较少

补充:服务进程

  • 由 Manager() 返回的管理器对象控制一个服务进程,该进程保存Python对象并允许其他进程使用代理操作它们。

  • Manager() 返回的管理器支持类型: list 、 dict 、 Namespace 、 Lock 、 RLock 、 Semaphore 、 BoundedSemaphore 、 Condition 、 Event 、 Barrier 、 Queue 、 Value 和 Array 。

  • 使用服务进程的管理器比使用共享内存对象更灵活,因为它们可以支持任意对象类型。此外,单个管理器可以通过网络由不同计算机上的进程共享。但是,它们比使用共享内存慢。

from multiprocessing import Process, Manager

def f(d, l):
    d[1] = '1'
    d['2'] = 2
    d[0.25] = None
    l.reverse()

if __name__ == '__main__':
    with Manager() as manager:
        d = manager.dict()
        l = manager.list(range(10))

        p = Process(target=f, args=(d, l))
        p.start()
        p.join()

        print(d)  # {0.25: None, 1: '1', '2': 2}
        print(l)  # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

文档参考:docs.python.org/zh-cn/3.9/l…

个人学习,如有谬误,欢迎指正。