Can I somehow share an asynchronous queue with a subprocess?
multiprocessing, python, python-asyncio, queue, shared-memory
Solution
Here is an implementation of a `multiprocessing.Queue` object that can be used with `asyncio`. It provides the entire `multiprocessing.Queue` interface, with the addition of `coro_get` and `coro_put` methods, which are `asyncio.coroutine`s that can be used to asynchronously get/put from/into the queue. The implementation details are essentially the same as the second example of my other answer: `ThreadPoolExecutor` is used to make the get/put asynchronous, and a `multiprocessing.managers.SyncManager.Queue` is used to share the queue between processes. The only additional trick is implementing `__getstate__` to keep the object picklable despite using a non-picklable `ThreadPoolExecutor` as an instance variable.
from multiprocessing import Manager, cpu_count
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
def AsyncProcessQueue(maxsize=0):
m = Manager()
q = m.Queue(maxsize=maxsize)
return _ProcQueue(q)
class _ProcQueue(object):
def __init__(self, q):
self._queue = q
self._real_executor = None
self._cancelled_join = False
@property
def _executor(self):
if not self._real_executor:
self._real_executor = ThreadPoolExecutor(max_workers=cpu_count())
return self._real_executor
def __getstate__(self):
self_dict = self.__dict__
self_dict['_real_executor'] = None
return self_dict
def __getattr__(self, name):
if name in ['qsize', 'empty', 'full', 'put', 'put_nowait',
'get', 'get_nowait', 'close']:
return getattr(self._queue, name)
else:
raise AttributeError("'%s' object has no attribute '%s'" %
(self.__class__.__name__, name))
@asyncio.coroutine
def coro_put(self, item):
loop = asyncio.get_event_loop()
return (yield from loop.run_in_executor(self._executor, self.put, item))
@asyncio.coroutine
def coro_get(self):
loop = asyncio.get_event_loop()
return (yield from loop.run_in_executor(self._executor, self.get))
def cancel_join_thread(self):
self._cancelled_join = True
self._queue.cancel_join_thread()
def join_thread(self):
self._queue.join_thread()
if self._real_executor and not self._cancelled_join:
self._real_executor.shutdown()
@asyncio.coroutine
def _do_coro_proc_work(q, stuff, stuff2):
ok = stuff + stuff2
print("Passing %s to parent" % ok)
yield from q.coro_put(ok) # Non-blocking
item = q.get() # Can be used with the normal blocking API, too
print("got %s back from parent" % item)
def do_coro_proc_work(q, stuff, stuff2):
loop = asyncio.get_event_loop()
loop.run_until_complete(_do_coro_proc_work(q, stuff, stuff2))
@asyncio.coroutine
def do_work(q):
loop.run_in_executor(ProcessPoolExecutor(max_workers=1),
do_coro_proc_work, q, 1, 2)
item = yield from q.coro_get()
print("Got %s from worker" % item)
item = item + 25
q.put(item)
if __name__ == "__main__":
q = AsyncProcessQueue()
loop = asyncio.get_event_loop()
loop.run_until_complete(do_work(q))
Output:
Passing 3 to parent
Got 3 from worker
got 28 back from parent
As you can see, you can use the `AsyncProcessQueue` both synchronously and asynchronously, from either the parent or child process. It doesn't require any global state, and by encapsulating most of the complexity in a class, is more elegant to use than my original answer.
You'll probably be able to get better performance using sockets directly, but getting that working in a cross-platform way seems to be pretty tricky. This also has the advantage of being usable across multiple workers, won't require you to pickle/unpickle yourself, etc.
Problem
I would like to use a queue for passing data from a parent to a child process which is launched via `multiprocessing.Process`. However, since the parent process uses Python's new `asyncio` library, the queue methods need to be non-blocking. As far as I understand, `asyncio.Queue` is made for inter-task communication and cannot be used for inter-process communication. Also, I know that `multiprocessing.Queue` has the `put_nowait()` and `get_nowait()` methods but I actually need coroutines that would still block the current task (but not the whole process). Is there some way to create coroutines that wrap `put_nowait()`/`get_nowait()`? On another note, are the threads that `multiprocessing.Queue` uses internally compatible after all with an event loop running in the same process? If not, what other options do I have? I know I could implement such a queue myself by making use of asynchronous sockets but I hoped I could avoid that… EDIT: I also considered using pipes instead of sockets but it seems `asyncio` is not compatible with `multiprocessing.Pipe()`. More precisely, `Pipe()` returns a tuple of `Connection` objects which are not file-like objects. However, `asyncio.BaseEventLoop`'s methods `add_reader()`/`add_writer()` methods and `connect_read_pipe()`/`connect_write_pipe()` all expect file-like objects, so it is impossible to asynchronously read from/write to such a `Connection`. In contrast, the usual file-like objects that the `subprocess` package uses as pipes pose no problem at all and can easily be used in combination with `asyncio`. UPDATE: I decided to explore the pipe approach a bit further: I converted the `Connection` objects returned by `multiprocessing.Pipe()` into file-like objects by retrieving the file descriptor via `fileno()` and passing it to `os.fdopen()`. Finally, I passed the resulting file-like object to the event loop's `connect_read_pipe()`/`connect_write_pipe()`. (There is some mailing list discussion on a related issue if someone is interested in the exact code.) However, `read()`ing the stream gave me an `OSError: [Errno 9] Bad file descriptor` and I didn't manage to fix this. Also considering the missing support for Windows, I will not pursue this any further.