【问题标题】:How is the multiprocessing.Queue instance serialized when passed as an argument to a multiprocessing.Process?multiprocessing.Queue 实例作为参数传递给 multiprocessing.Process 时如何序列化?
【发布时间】:2023-02-09 07:24:47
【问题描述】:

Why I can't use multiprocessing.Queue with ProcessPoolExecutor? 提出了一个相关问题。我提供了部分答案和解决方法,但承认该问题引发了另一个问题,即为什么 multiprocessing.Queue 实例作为参数传递给 multiprocessing.Process worker 函数。

例如,以下代码在使用产卵或者创建新进程的方法:

from multiprocessing import Pool, Queue

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

with Pool(1) as pool:
    q = Queue()
    q.put(7)
    pool.apply(worker, args=(q,))

以上提出:

RuntimeError: Queue objects should only be shared between processes through inheritance

然而下面的程序运行没有问题:

from multiprocessing import Process, Queue

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

q = Queue()
q.put(7)
p = Process(target=worker, args=(q,))
p.start()
p.join()

似乎多处理池工作函数的参数最终被放入池的输入队列中,该队列被实现为 multiprocessing.Queue,并且您不能将 multiprocessing.Queue 实例放入使用 ForkingPicklermultiprocessing.Queue 实例用于序列化。

那么,当 multiprocessing.Queue 作为参数传递给允许以这种方式使用的 multiprocessing.Process 时,它是如何序列化的呢?

【问题讨论】:

    标签: python multiprocessing queue


    【解决方案1】:

    我想在 accepted answer 上进行扩展,所以我添加了自己的内容,其中还详细介绍了一种使队列、锁等可挑选并能够通过池发送的方法。

    为什么会这样

    基本上,并不是队列不能序列化,只是 multiprocessing 只有在知道有关它将被发送到的目标进程(无论是当前进程还是其他进程)的足够信息时才能够序列化这些队列,这就是为什么当您自己生成一个进程(使用Process 类)时它会起作用,但当您只是将它放入队列时(例如使用Pool 时)则不起作用。

    查看multiprocessing.queues.Queue(或其他连接对象,如Condition)的源代码。你会发现在他们的__getstate__方法(队列实例被pickle时调用的方法)中,有一个函数multiprocessing.context.assert_spawning的调用。这个“断言”只有在当前线程正在产生一个过程。如果不是这种情况,multiprocessing 会引发您看到的错误并退出。

    现在 multiprocessing 甚至懒得去腌制队列以防断言失败的原因是它无法访问线程创建子进程时创建的 Popen 对象(对于 Windows,您可以在 multiprocessing.popen_spawn_win32.Popen 找到它).该对象存储有关目标进程的数据,包括其 pid 和进程句柄。多处理需要此信息,因为 Queue 包含互斥体,并且要成功地 pickle 并稍后再次重建这些,多处理必须使用来自 Popen 对象的信息通过 winapi 调用 DuplicateHandle。如果没有此对象,多处理将不知道该做什么并引发错误。所以这就是我们的问题所在,但如果我们可以教 multiprocessing 一种不同的方法来从目标进程本身内部窃取重复句柄,而无需事先要求它的信息,那么它是可以解决的。

    制作可挑选队列

    关注班级multiprocessing.synchronize.SemLock。它是所有多处理锁的基类,所以它的对象随后出现在队列、管道等中。它当前被 pickle 的方式就像我上面描述的那样,它需要目标进程的句柄来创建一个重复的句柄。但是,我们可以为 SemLock 定义一个 __reduce__ 方法,我们将使用当前进程的句柄创建一个重复的句柄,然后从目标进程复制先前创建的句柄,该句柄现在在目标进程的上下文中有效.这相当冗长,但实际上也使用类似的方法来腌制 PipeConnection 对象,但它使用 dispatch table 来代替 __reduce__ 方法。

    完成后,我们可以继承Queue 并删除对assert_spawning 的调用,因为不再需要它。这样,我们现在就可以成功地 pickle 锁、队列、管道等。下面是带有示例的代码:

    import os, pickle
    from multiprocessing import Pool, Lock, synchronize, get_context
    import multiprocessing.queues
    import _winapi
    
    
    def work(q):
        print("Worker: Main says", q.get())
        q.put('haha')
    
    
    class DupSemLockHandle(object):
        """
        Picklable wrapper for a handle. Attempts to mirror how PipeConnection objects are pickled using appropriate api
        """
    
        def __init__(self, handle, pid=None):
            if pid is None:
                # We just duplicate the handle in the current process and
                # let the receiving process steal the handle.
                pid = os.getpid()
            proc = _winapi.OpenProcess(_winapi.PROCESS_DUP_HANDLE, False, pid)
            try:
                self._handle = _winapi.DuplicateHandle(
                    _winapi.GetCurrentProcess(),
                    handle, proc, 0, False, _winapi.DUPLICATE_SAME_ACCESS)
            finally:
                _winapi.CloseHandle(proc)
            self._pid = pid
    
        def detach(self):
            """
            Get the handle, typically from another process
            """
            # retrieve handle from process which currently owns it
            if self._pid == os.getpid():
                # The handle has already been duplicated for this process.
                return self._handle
            # We must steal the handle from the process whose pid is self._pid.
            proc = _winapi.OpenProcess(_winapi.PROCESS_DUP_HANDLE, False,
                                       self._pid)
            try:
                return _winapi.DuplicateHandle(
                    proc, self._handle, _winapi.GetCurrentProcess(),
                    0, False, _winapi.DUPLICATE_CLOSE_SOURCE | _winapi.DUPLICATE_SAME_ACCESS)
            finally:
                _winapi.CloseHandle(proc)
    
    
    def reduce_lock_connection(self):
        sl = self._semlock
        dh = DupSemLockHandle(sl.handle)
        return rebuild_lock_connection, (dh, type(self), (sl.kind, sl.maxvalue, sl.name))
    
    
    def rebuild_lock_connection(dh, t, state):
        handle = dh.detach()  # Duplicated handle valid in current process's context
    
        # Create a new instance without calling __init__ because we'll supply the state ourselves
        lck = t.__new__(t)
        lck.__setstate__((handle,)+state)
        return lck
    
    
    # Add our own reduce function to pickle SemLock and it's child classes
    synchronize.SemLock.__reduce__ = reduce_lock_connection
    
    
    class PicklableQueue(multiprocessing.queues.Queue):
        """
        A picklable Queue that skips the call to context.assert_spawning because it's no longer needed
        """
    
        def __init__(self, *args, **kwargs):
            ctx = get_context()
            super().__init__(*args, **kwargs, ctx=ctx)
    
        def __getstate__(self):
    
            return (self._ignore_epipe, self._maxsize, self._reader, self._writer,
                    self._rlock, self._wlock, self._sem, self._opid)
    
    
    def is_locked(l):
        """
        Returns whether the given lock is acquired or not.
        """
        locked = l.acquire(block=False)
        if locked is False:
            return True
        else:
            l.release()
            return False
    
    
    if __name__ == '__main__':
    
        # Example that shows that you can now pickle/unpickle locks and they'll still point towards the same object
        l1 = Lock()
        p = pickle.dumps(l1)
        l2 = pickle.loads(p)
        print('before acquiring, l1 locked:', is_locked(l1), 'l2 locked', is_locked(l2))
        l2.acquire()
        print('after acquiring l1 locked:', is_locked(l1), 'l2 locked', is_locked(l2))
    
        # Example that shows how you can pass a queue to Pool and it will work
        with Pool() as pool:
    
            q = PicklableQueue()
            q.put('laugh')
            pool.map(work, (q,))
            print("Main: Worker says", q.get())
    

    输出

    before acquiring, l1 locked: False l2 locked False
    after acquiring l1 locked: True l2 locked True
    Worker: Main says laugh
    Main: Worker says haha
    

    免责声明: 以上代码仅适用于 Windows。如果您使用的是 UNIX,那么您可以尝试使用 @Booboo's 修改后的代码(报告有效但尚未经过充分测试,完整代码链接 here):

    import os, pickle
    from multiprocessing import Pool, Lock, synchronize, get_context, Process
    import multiprocessing.queues
    import sys
    _is_windows= sys.platform == 'win32'
    if _is_windows:
        import _winapi
    
    .
    .
    .
    
    class DupSemLockHandle(object):
        """
        Picklable wrapper for a handle. Attempts to mirror how PipeConnection objects are pickled using appropriate api
        """
    
        def __init__(self, handle, pid=None):
            if pid is None:
                # We just duplicate the handle in the current process and
                # let the receiving process steal the handle.
                pid = os.getpid()
            if _is_windows:
                proc = _winapi.OpenProcess(_winapi.PROCESS_DUP_HANDLE, False, pid)
                try:
                    self._handle = _winapi.DuplicateHandle(
                        _winapi.GetCurrentProcess(),
                        handle, proc, 0, False, _winapi.DUPLICATE_SAME_ACCESS)
                finally:
                    _winapi.CloseHandle(proc)
            else:
                self._handle = handle
            self._pid = pid
    
        def detach(self):
            """
            Get the handle, typically from another process
            """
            # retrieve handle from process which currently owns it
            if self._pid == os.getpid():
                # The handle has already been duplicated for this process.
                return self._handle
    
            if not _is_windows:
                return self._handle
    
            # We must steal the handle from the process whose pid is self._pid.
            proc = _winapi.OpenProcess(_winapi.PROCESS_DUP_HANDLE, False,
                                       self._pid)
            try:
                return _winapi.DuplicateHandle(
                    proc, self._handle, _winapi.GetCurrentProcess(),
                    0, False, _winapi.DUPLICATE_CLOSE_SOURCE | _winapi.DUPLICATE_SAME_ACCESS)
            finally:
                _winapi.CloseHandle(proc)
    

    【讨论】:

    • 很有意思。它显然不适用于 Linux,因为 _winapi 不存在。
    • 这样做很愚蠢,因为我永远不会使用该代码,但我相信经过一些修改(我还添加了一些额外的测试)后我让你的代码可以在 Linux 上工作。在您尝试加入子进程之前,get 函数work 回复的消息是必要的,否则您可能会挂起。因此,为了使逻辑更简单(您不想在子进程完成其get 之前尝试获取答复,否则它将挂起),我提供了一个单独的答复队列。参见demo。您可以根据需要随意更新您的答案。
    • @Booboo 你能确认代码在 Linux 上对 spawn 和 fork 都有效吗?
    • 似乎跟...共事产卵,但它会永远。你应该审查它。参见new demo
    • @Booboo 我查看了代码,我不确定生成的方法将如何影响句柄在 Linux 上的传递方式,我现在也没有办法对其进行测试。我将在我的答案中包含该演示并添加免责声明。
    【解决方案2】:

    multiprocessing.Qeue 序列化为 multiprocessing.Process.run 方法时,序列化的不是队列本身。队列由一个打开的管道(类型取决于平台)实现,由一个文件描述符表示,以及一个序列化访问管道的锁。它是被序列化/反序列化的文件描述符和锁,然后可以从中重建原始队列。

    【讨论】:

    • 您是说将队列传递给在不同地址空间中执行的 multiprocessing.Process.run 方法时它起作用的原因是因为它是不是正在序列化的队列本身,而是实现队列的相关管道文件描述符和信号量?如果是这样,那是您唯一需要的答案,即一句话。第一段和最后一段是不必要的,并且有损于答案的本质。
    • @Booboo 是的,基本上就是这样,我修改了答案以将其减少到仅必要的位。
    • 我修改了您的答案以使其更准确。如果我犯了错误,请更正。
    • 我不确定我的问题是否已被充分地回答。 multiprocessing.Queue 实现了自定义的 __getstate____setstate__ pickle 方法,正如您所期望的那样,它在 __getstate__ 中,有一个通过调用 context.assert_spawning 进行的测试,当它是未出于序列化 Process 实例的目的而被序列化(队列使用的类 RLock 也是如此)。这似乎是任意的。如果不进行此检查以便将 Queue 写入 Queue,会有什么危险?
    • @Booboo 传递一个信号量不是微不足道的,在 Windows 上它需要通过调用 DuplicateHandle 来完成,这需要父进程和子进程句柄,您可以使用命名的信号量创建自己的可序列化队列,并且命名管道,而不是让操作系统处理没有句柄的链接,但队列的当前实现不允许这样做,老实说,除了在 multiprocessing.Pool 中使用队列之外,没有理由为什么该队列应该是可序列化的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-10-28
    • 1970-01-01
    • 2020-05-12
    相关资源
    最近更新 更多