在多进程中共享异步 - 等待基于协同程序的复杂对象

时间:2018-02-01 04:37:54

标签: python python-3.x asynchronous multiprocessing python-asyncio

我一般都知道,不应该在多进程之间共享对象以及可能由此产生的问题。但我的要求是这样做是必要的。

我有一个复杂的对象,其中包含所有漂亮的协同程序async-await。 一个函数,它在自己的单独进程中对此对象运行长时间运行的进程。现在,我想在主进程中运行一个IPython shell,并对这个复杂的对象进行操作,而长时间运行的进程正在另一个进程中运行。

为了跨进程共享这个复杂的对象,我尝试了在SO上遇到的多处理BaseManager方法:

import multiprocessing
import multiprocessing.managers as m


class MyManager(m.BaseManager):
    pass

MyManager.register('complex_asynio_based_class', complex_asynio_based_class)
manager = MyManager()
manager.start()
c = manager.complex_asynio_based_class()

process = multiprocessing.Process(
     target=long_running_process,
     args=(c,),
)

但这会产生错误:

Unserializable message: Traceback (most recent call last):
  File "/usr/3.6/lib/python3.6/multiprocessing/managers.py", line 283, in serve_client
    send(msg)
  File "/usr/3.6/lib/python3.6/multiprocessing/connection.py", line 206, in send
    self._send_bytes(_ForkingPickler.dumps(obj))
  File "/usr/3.6/lib/python3.6/multiprocessing/reduction.py", line 51, in dumps
    cls(buf, protocol).dump(obj)
TypeError: can't pickle coroutine objects

由于对象中存在协同程序,因此无法正常工作。我无法想出一个更好的解决方案来让它发挥作用,我坚持下去。

如果不是Python,我会为长时间运行的过程生成一个线程,但仍然可以对它进行操作。

如果我没有错,那么这应该是多进程应用程序运行后台进程的常用模式,以及只对它执行一些只读操作的主进程,就像我的情况一样,而不是修改它。我想知道它是如何完成的?

如何在多进程中共享无法挑选的复杂对象?

2 个答案:

答案 0 :(得分:5)

运行协程不能在进程间自动共享,因为协程在拥有异步类的进程中的特定事件循环内运行。协程具有无法腌制的状态,即使可以,它在事件循环的上下文之外也没有意义。

你可以做的是为异步类创建一个基于回调的适配器,每个协程方法由一个基于回调的方法表示,语义为"开始执行X并在完成后调用此函数&#34 ;。如果回调是多处理感知的,则可以从其他进程调用这些操作。然后,您可以在每个进程中启动一个事件循环,并在代理的基于回调的调用上创建一个协程外观。

例如,考虑一个简单的异步类:

class Async:
    async def repeat(self, n, s):
        for i in range(n):
            print(s, i, os.getpid())
            await asyncio.sleep(.2)
        return s

基于回调的适配器可以使用公共asyncio API将repeat协同程序转换为JavaScript中的经典异步函数"回调地狱"式:

class CallbackAdapter:
    def repeat_start(self, n, s, on_success):
        fut = asyncio.run_coroutine_threadsafe(
            self._async.repeat(n, s), self._loop)
        # Once the coroutine is done, notify the caller.
        fut.add_done_callback(lambda _f: on_success(fut.result()))

(转换可以自动进行,上面手动编写的代码只显示了这个概念。)

CallbackAdapter可以通过多处理注册,因此不同的进程可以通过多处理提供的代理启动适配器的方法(因此也就是原始的异步协同程序)。这只要求作为on_success传递的回调是多处理友好的。

作为最后一步,可以完全循环并为基于回调的API(!)创建异步适配器,在其他进程中启动事件循环,并使用asyncio和{{1} }。这个适配器适配器类将运行一个功能齐全的async def协程,它可以有效地代理原始的repeat协程,而不会试图腌制协程状态。

以下是上述方法的示例实现:

Async.repeat

代码在Python 3.5上正确运行,但由于a bug in multiprocessing而在3.6和3.7上失败。

答案 1 :(得分:0)

I have been using multiprocessing module and asyncio module for a litte while.

You don't share objects between processes. You create an object(referent) in one process, return a proxy object and share it with other process. Other process use proxy object to invoke referent's methods.

In your code, the referent is the complex_asynio_based_class instance.

Here is the stupid code you can refer. THe main thread is a single asyncio loop running UDP server and severl other async ops. The long running process simple checks the loop status.

import multiprocessing
import multiprocessing.managers as m
import asyncio 
import logging
import time 

logging.basicConfig(filename="main.log", level=logging.DEBUG) 

class MyManager(m.BaseManager):
    pass

class sinkServer(asyncio.Protocol):


    def connection_made(self, transport):
        self.transport = transport

    def datagram_received(self, data, addr):
        message = data.decode()
        logging.info('Data received: {!r}'.format(message))


class complex_asynio_based_class:

    def __init__(self, addr=('127.0.0.1', '8080')):
        self.loop = asyncio.new_event_loop() 
        listen = self.loop.create_datagram_endpoint(sinkServer, local_addr=addr,
                    reuse_address=True, reuse_port=True)
        self.loop.run_until_complete(listen)
        for name, delay in zip("abcdef", (1,2,3,4,5,6)):
            self.loop.run_until_complete(self.slow_op(name, delay))

    def run(self):
        self.loop.run_forever() 

    def stop(self):
        self.loop.stop() 

    def is_running(self):
        return self.loop.is_running() 

    async def slow_op(self, name, delay):
        logging.info("my name: {}".format(name))
        asyncio.sleep(delay)

def long_running_process(co):
    logging.debug('address: {!r}'.format(co))
    logging.debug("status: {}".format(co.is_running()))
    time.sleep(6)
    logging.debug("status: {}".format(co.is_running()))

MyManager.register('complex_asynio_based_class', complex_asynio_based_class)
manager = MyManager()
manager.start()
c = manager.complex_asynio_based_class()

process = multiprocessing.Process(
     target=long_running_process,
     args=(c,),
)
process.start()

c.run()  #run the loop