Before we see how to inject threads or processes in to an asynchronous event loop, we will take a closer look at the concurrent.futures module that will later be the main ingredient of our so-called workaround.
The most important classes in the concurrent.futures module are Executor and Future.
Executor represents a pool of resources that may process work items in parallel. This may seem very similar in purpose to classes from the multiprocessing module—Pool and dummy.Pool—but it has a completely different interface and semantics. It is a base class not intended for instantiation and has the following two concrete implementations:
- ThreadPoolExecutor: This is the one that represents a pool of threads
- ProcessPoolExecutor: This is the one that represents a pool of processes
Every executor provides the following three methods:
- submit(func, *args, **kwargs): This schedules the func function for execution in a pool of resources and returns the Future object representing the execution of a callable
- map(func, *iterables, timeout=None, chunksize=1): This executes the func function over iterable in a similar way to the multiprocessing.Pool.map() method
- shutdown(wait=True): This shuts down the executor and frees all of its resources
The most interesting method is submit() because of the Future object it returns. It represents asynchronous execution of the callable and only indirectly represents its result. In order to obtain the actual return value of the submitted callable, you need to call the Future.result() method. And if the callable has already finished, the result() method will not block and will just return the function output. If it is not true, it will block until the result is ready. Treat it like a promise of a result (actually, it is the same concept as a promise in JavaScript). You don't need to unpack it immediately after receiving it (with the result() method), but if you try to do that, it is guaranteed to eventually return something like the following:
>>> def loudly_return(): ... print("processing") ... return 42 ... >>> from concurrent.futures import ThreadPoolExecutor >>> with ThreadPoolExecutor(1) as executor: ... future = executor.submit(loudly_return) ... processing >>> future <Future at 0x33cbf98 state=finished returned int> >>> future.result() 42
If you want to use the Executor.map() method, it does not differ in usage from the Pool.map() method of the pool class from the multiprocessing module, as follows:
def main(): with ThreadPoolExecutor(POOL_SIZE) as pool: results = pool.map(fetch_rates, BASES) for result in results: present_result(*result)
In the next section, we'll see how to use executors in an event loop.