Programmer's Python: Async - Futures
Written by Mike James   
Monday, 28 August 2023
Article Index
Programmer's Python: Async - Futures
Executors
Locking and Sharing Data
Using a Process Manager to Share Resources

Python has promises -  it's just that they are called "futures" - and they are the best way to get results back to your main program. Find out how to use a future in this extract from Programmer's Python: Async.

Programmer's Python:
Async
Threads, processes, asyncio & more

Is now available as a print book: Amazon

pythonAsync360Contents

1)  A Lightning Tour of Python.

2) Asynchronous Explained

3) Processed-Based Parallelism
         Extract 1 Process Based Parallism
4) Threads
         Extract 1 -- Threads
5) Locks and Deadlock

6) Synchronization

7) Sharing Data
        Extract 1 - Pipes & Queues

8) The Process Pool
        Extract 1 -The Process Pool 1 ***NEW!

9) Process Managers

10) Subprocesses

11) Futures
        Extract 1 Futures

12) Basic Asyncio
        Extract 1 Basic Asyncio

13) Using asyncio
        Extract 1 Asyncio Web Client
14) The Low-Level API
       Extract 1 - Streams & Web Clients
Appendix I Python in Visual Studio Code

 

So far we have dealt with the problem of coordinating what happens to the results of asynchronous programs as a low-level concern or as no problem at all. In practice, getting asynchronous results is a major distortion of the usual logic of a program and “futures” are an attempt to restore this logic.

Futures are called “promises” or “deferred” objects in other languages and Python does things differently, as you might expect, in that it automates the way that futures are generated. In other languages, it is up to the programmer to create functions which return a promise object. In Python a standard function is automatically converted to return a Future just by the way that it is invoked.

Another important difference is that in most cases promises are implemented in a single-threaded environment. The concurrent.futures module is a multi-threading, multi-process implementation of a futures-based framework built on top of the multiprocessing module. The ability to use futures in a multi-threaded/process environment is unusual and very useful.

The asyncio module also implements futures, but in a more restricted and more conventional single-threaded environment. In a single-threaded environment futures are even more important because they solve the problem of keeping the single thread free to get on with other tasks while preserving much of the logical structure of the program.

In a multi-threaded, multi-process environment futures are still very useful but they are not as essential. They provide a clean way for asynchronous code to return results and handle errors.

In this chapter we look at multi-threaded, multi-process-based futures and their associated executors. The executors are used from within asyncio so they are worth knowing about, even if you don’t plan to use concurrent.futures directly.

Futures

We have already encountered a basic future-style object in the form of the AsyncResult object used in connection with the process pool, see Chapter 8. The concurrent.futures module has a more complete and general implementation of a Future object.

In most cases you don’t have to worry about creating a Future as they are automatically returned from asynchronous functions run by one of the module’s executor objects. The concurrent.futures module adds futures to your functions without you having to do anything at all. That means you can write a function that simply raises exceptions when errors occur and returns a result in the usual way and if you use it with concurrent.futures it will automatically return a Future which handles both the results and the exceptions.

Futures are a better alternative to the ad-hoc approach of returning and waiting for results from asynchronous programs. When we move on to look at futures in a single-threaded environment we will discover that they also lead on to higher-level ideas such as await.

The basic idea is that a Future is an object that lives in the main thread/process that the child thread/process can use to signal its state and to return a result at some time in the future, hence the name.

A Future can be in one of three states:

  • Resolved – the function has completed and returned a result, which may be an exception, which is now available in the Future

  • Pending – the function is still running and no result is available yet

  • Canceled – the function has been canceled and an exception has been returned

The main thread/process can make use of the Future via its methods.

  • result(timeout=None) - waits for a result to be ready
    If the call times out a TimeoutError is raised. If the Future is canceled a CancelledError is raised. If the call completed its result is returned by the Future.
  • exception(timeout=None) - returns the exception raised by the call, TimeoutError or CancelledError
  • cancel() - tries to cancel the call. You can only cancel a call if it isn’t running, i.e. it is still in the queue waiting for a thread/process from the pool to be available. If you want to cancel a running call then you need to implement a signaling mechanism and deal with any clean up that is required.
  • cancelled() - True if the call has been successfully canceled
  • done() - True if the task is complete or successfully canceled
  • running() - True if the call is executing and cannot be canceled

Although it seems like a retrograde step back to callbacks you can also use add_done__callback(function) which attaches the function as a callback when the call is done, canceled or has finished running. The callback function receives the Future as its only argument and this can be used to retrieve the result of the call.

Notice that add_done_callback has the advantage that there is no need for the function being run asynchronously to support callbacks, it is all handled by the framework.



Last Updated ( Monday, 28 August 2023 )