Note: this is a translation; the original is Combining Coroutines with Threads and Processes
Many existing Python libraries are not yet ready to work with asyncio. They may block, or depend on concurrency features the module doesn’t provide. It is still possible to use these libraries in asyncio-based programs. The method is to use the executors provided by concurrent.futures to run this code in separate threads or processes.
Threads
The run_in_executor method in the event loop takes an executor object, a callable object,
and some arguments to pass. It returns a Future object, which can wait for the function to finish
and pass back the return value. If we don’t pass an executor object, a ThreadPoolExecutor will
be created; the example below explicitly creates the executor to limit the maximum number of concurrent threads.
A ThreadPoolExecutor starts its own threads and then calls each function passed in within a thread.
This example shows how to combine run_in_executor() with wait() so the event loop still has the ability to yield
while these blocking functions run, and when the functions finish, the event loop
will reactivate the caller.
1 | # asyncio_executor_thread.py |
asyncio_executor_thread.py uses logging to conveniently show which function or thread generated the record.
Because each blocking function calls a separate logger, the output also shows some threads being reused
to complete the work.
1 | $ python asyncio_exector_thread.py |
Processes
A ProcessPoolExecutor works similarly to the above, except it creates worker processes instead of threads.
Using separate processes requires more system resources, but for compute-intensive operations it can fully utilize
each CPU core.
1 | # asyncio_executor_process.py |
The only difference in the code concerning processes is creating a different type of executor. This example also changes the log format,
printing the process id instead of the thread name, to show that the tasks run in separate processes.
1 | $ python asyncio_exector_process.py |
My Understanding and Extension
The first part of the article about threads gives the feeling of a thread pool. First, you create some threads — the executor is a kind of pool; here only a maximum is given — and then submit tasks. The difference is that here await is used to glue the thread results to the context of the current event loop thread, avoiding writing callbacks.
Maybe this really is a transitional approach — forcibly extending support to libraries that temporarily don’t support asyncio.
The official documentation‘s introduction of run_in_executor, compared with the original blog above, may be
more basic; for normal usage it’s still more suitable to follow the example above.