Last Updated on September 12, 2022
Future objects are a promise for a result from an asynchronous task executed by the ThreadPoolExecutor.
In this tutorial, you will discover Future objects used by Python thread pools.
Let’s get started.
ThreadPoolExecutor Returns Futures
The ThreadPoolExecutor in Python provides a pool of reusable threads for executing ad hoc tasks.
You can submit tasks to the thread pool by calling the submit() function and passing in the name of the function you wish to execute on another thread.
Calling the submit() function will return a Future object.
What are Future objects returned by the ThreadPoolExecutor?
Run loops using all CPUs, download your FREE book to learn how.
Future Is a Handle on an Asynchronous Task
A Future object represents the asynchronous execution of a task.
Get a Future Object
You will not create a Future object yourself.
Instead, you will receive a Future object when calling the submit() on your ThreadPoolExecutor.
1 2 3 |
... # submit a task to the thread pool future = executor.submit(work) |
The idea is that you hang on to the Future object and query it to check on the status of your task.
If you do not need to query the status of your asynchronous task or retrieve a result, you do not need to keep the Future object returned from a call on submit.
Future Object Status
You can check on the status of your asynchronous task via its Future object.
For example, you may want to check on the status of your task, such as whether it is currently running, is done, or perhaps has been cancelled.
This can be achieved by calling functions on the Future object; for example:
1 2 3 4 |
... # check if the task is running if future.running(): # do something... |
The three functions you can use to check the status of your task are running(), done(), and cancelled().
- running() Returns True if the task is currently running, False otherwise.
- done() Returns True if the task has completed, False otherwise.
- cancelled() Returns True if the task was cancelled, False otherwise.
Future Object Results
You can also use the Future objects to get the result from the task or the exception if one was raised during the execution of the task.
This can be achieved by calling the result() function for the result and the exception() function to retrieve the exception.
- result() Returns the result of the target task function once complete, or None if the function does not return a value.
- exception() Returns the exception raised during the execution of the task once complete, or None if no exception was raised.
The result() and exception() functions only return once the task has completed, e.g. done() returns True.
This means that the calls to result() and exception will block until the task is completed. That is, the call will automatically wait until the task is complete before returning a value.
1 2 3 |
... # get a result from the task once it is complete result = future.result() # blocks |
Future Object Timeouts
It is a good practice to limit how long you are willing to wait for a result or an exception.
As such, you can set a timeout when calling these functions via the timeout argument and specify a number of seconds.
If the timeout elapses before a result or exception is returned, then a TimeoutError is raised that you may choose to handle.
1 2 3 4 5 6 7 8 |
... # handle any timeout try: # get a result from the task once it is complete result = future.result(timeout=60) # blocks # do something... except TimeoutError: # handle timeout |
Future Object Exception Handling
If an exception is raised during the execution of the task, it will be raised again automatically when you attempt to retrieve the result from the Future.
As such, if an exception can reasonably be raised within the task, then you can handle it when retrieving the result.
1 2 3 4 5 6 7 8 |
… # handle exception raised by the task try: # get a result from the task once it is complete result = future.result() # blocks # do something... except: # handle exception raised when executing the task |
Future Object Callbacks
The Future allows us to register a callback function to be called once the task has completed.
This can be achieved by calling the add_done_callback() function in the future and specifying the name of our custom callback function.
1 2 3 |
... # register a callback function future.add_done_callback(custom_callback) |
Our custom callback function must take a single argument, which is the future object on which it is registered.
1 2 |
def custom_callback(future): # do something |
The callback is only called once the task has completed. You can register multiple callback functions for a given Future object and they will be called in the order that they were registered.
An exception in one callback function will not impact the calls to subsequent callback functions.
Multiple Future Objects
If you call submit multiple times for different tasks or different arguments to the same task, you can do so in a loop and store all of the Future objects in a collection for later use.
For example, it is common to use a list comprehension.
1 2 3 |
... # create many tasks and store the future objects in a list futures = [executor.submit(work) for _ in range(100)] |
The collection of Future objects can then be handed off to utility functions provided by the concurrent.futures module, such as wait() and as_completed().
Now that we are familiar with how to use Future objects, let’s take a closer look at the life-cycle of Futures.
Life-cycle of Future Objects
A Future object is created when we call submit() for a task on a ThreadPoolExecutor.
A Future object can exist in one of three states:
- Scheduled (pre-running)
- Running
- Done (post-running)
The figure below summarizes the life-cycle of a Future object.
Scheduled Future Object
After the Future object is created, it is queued in the thread pool for execution until a worker thread becomes available to execute it.
At this point, it is not “running.” It is “scheduled.”
A scheduled task can be “cancelled.”
Running Future Object
A worker thread will take a task off the internal queue and start executing it.
Once a task has started being executed, the status of the Future object is now “running.”
A running task cannot be cancelled.
Done Future Object
When the task for a Future object completes, it has the status “done,” and if the target function returns a value, it can be retrieved.
A “done” task will not be “running.”
While a task is running, it can raise an uncaught exception, causing the execution of the task to stop. The exception will be stored and can be retrieved directly or will be re-raised if the result is attempted to be retrieved.
A “cancelled” task will always be in the “done” state.
Free Python ThreadPoolExecutor Course
Download your FREE ThreadPoolExecutor PDF cheat sheet and get BONUS access to my free 7-day crash course on the ThreadPoolExecutor API.
Discover how to use the ThreadPoolExecutor class including how to configure the number of workers and how to execute tasks asynchronously.
Further Reading
This section provides additional resources that you may find helpful.
Books
- ThreadPoolExecutor Jump-Start, Jason Brownlee, (my book!)
- Concurrent Futures API Interview Questions
- ThreadPoolExecutor Class API Cheat Sheet
I also recommend specific chapters from the following books:
- Effective Python, Brett Slatkin, 2019.
- See Chapter 7: Concurrency and Parallelism
- Python in a Nutshell, Alex Martelli, et al., 2017.
- See: Chapter: 14: Threads and Processes
Guides
- Python ThreadPoolExecutor: The Complete Guide
- Python ProcessPoolExecutor: The Complete Guide
- Python Threading: The Complete Guide
- Python ThreadPool: The Complete Guide
APIs
References
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
Takeaways
You now know how to use Future objects returned from the ThreadPoolExecutor.
Do you have any questions about how to use Future objects?
Ask your question in the comments below and I will do my best to answer.
Photo by Berend Verheijen on Unsplash
Parag Chaudhari says
Thank You for sharing your expertise. Appreciate it a lot. This page helped me quickly grasp the ThreadPool concept, reverse engineer what an earlier programmer had developed and develop to a new recent requirement. Thanks again.
–Parag
Jason Brownlee says
You’re very welcome, I’m happy to hear that.