Bridging Python Asyncio and Concurrent Futures
Python, in an apparent break from the “There should be one—and preferably only
one—obvious way to do it” principle, has introduced two different kinds of
asyncio.Future. Even better, these
two are incompatible and represent fundamentally different models of concurrent
With the release of Python 3.5, Python added the
await keywords to
the language, as well as adding the
asyncio library into the standard library.
These were intended to make writing single-threaded event-loop-driven applications
easier to write—and they do! However, this created a confusing naming/purpose
conflict with the existing method of concurrent execution,
Future objects to aid in writing parallel code.
Future is a promise to resolve to a value at some point in the
future. This is useful for parallel and concurrent programming, where a
holds the future result of a computation dispatched to the pool. The difference
between these two is that in a
concurrent.futures.Future the computation occurs
on a separate process and happens concurrently with the rest of the jobs in
the pool, whereas with an
asyncio.Future the computation is performed on a
single thread with all the other computations in the pool in the asyncio event
loop. Broadly, the
concurrent.futures.Future construct is good for CPU-bound
operations, and the
asyncio.Future construct is good for I/O-bound operations.
Unfortunately, due to the differences in the underlying execution models these
Futures are incompatible with each other. For example, you cannot
async def async_function(future: concurrent.futures.Future): # can't do this! concurrent.futures.Future is not awaitable result = await future
You can kind of get around this by waiting on the result directly:
async def async_function(future: concurrent.futures.Future): # at least it doesn't throw an error now? result = future.result()
But this comes with a major drawback: while the future is pending,
until a result is available. This means that if you take this approach,
your entire async event loop will come to a grinding halt for the duration of
the call. Clearly this is not ideal.
Thankfully, there is another approach.
asyncio has the ability to run a task
in an executor and wrap it in a event-loop-compatible container. This handles
concurrent.future.Future for you. Here’s an example (adapted
from the standard library documentation):
async def main(): loop = asyncio.get_running_loop() with concurrent.futures.ProcessPoolExecutor() as pool: result = await loop.run_in_executor( pool, cpu_bound) print(result)
see more examples from the docs.
Even more easily, you can just use
asyncio.wrap_future()! From our example
async def async_function(future: concurrent.futures.Future): result = await asyncio.wrap_future(future)
This is useful if you need to port existing code to use an async event loop. As an anecdote, I used this approach to rewrite the web server portion of a data-intensive application without having to rewrite the (very complex) scheduler code.