You can issue one-off tasks to the ThreadPoolExecutor using the submit() method.
This returns a Future object that gives control over the asynchronous task executed in the thread pool.
In this tutorial, you will discover how to use the ThreadPoolExecutor submit() method.
Let’s get started.
What is the ThreadPoolExecutor
The ThreadPoolExecutor provides a pool of reusable worker threads using the executor design pattern.
Tasks executed in new threads are executed concurrently in Python, making the ThreadPoolExecutor appropriate for I/O-bound tasks.
A ThreadPoolExecutor can be created directly and then shut down to release all of the threads.
For example:
1 2 3 4 5 6 |
... # create the thread pool tpe = ThreadPoolExecutor() # ... # shutdown the thread pool tpe.shutdown() |
You can learn more about shutting down the thread pool in the tutorial:
Alternatively, we can use the context manager interface which will shut down the pool automatically for us when we are done with it.
For example:
1 2 3 4 5 |
... # create a thread pool with ThreadPoolExecutor() as tpe: # ... # shudown the pool automatically |
You can learn more about the ThreadPoolExecutor context manager in the tutorial:
We can issue tasks to the thread pool as one-off tasks via the submit() method or in batches using the map() method.
For example:
1 2 3 4 5 6 |
... # create a thread pool with ThreadPoolExecutor() as tpe: # issue tasks and report results for result in tpe.map(task, args): print(result) |
You can learn more about the ThreadPoolExecutor in the tutorial:
Now that we are familiar with what the ThreadPoolExecutor is, let’s take a closer look at the submit() method.
Run loops using all CPUs, download your FREE book to learn how.
What is ThreadPoolExecutor submit()
The ThreadPoolExecutor provides the submit() method.
This method can be used to issue tasks asynchronously to the thread pool. This means that the call requests that the ThreadPoolExecutor run the task as soon as it is able and returns immediately.
The task will execute sometime in the future.
The API documentation suggests that the submit() method “schedules” the task for execution.
Schedules the callable, fn, to be executed as fn(*args, **kwargs) and returns a Future object representing the execution of the callable.
— concurrent.futures — Launching parallel tasks
If you are new to the idea of executing asynchronous tasks, see the tutorial:
How to Use ThreadPoolExecutor submit()
The submit() method takes the name of a function to execute, and any argument for the function, and returns a Future object.
The submitted task is then executed by one of the workers in the ThreadPoolExecutor at some future time.
For example, we can execute the time.sleep() function with the argument 1 second in a worker thread in the ThreadPoolExecutor:
1 2 3 4 5 |
... # create a thread pool with ThreadPoolExecutor() as tpe: # issue the sleep function as a task future = tpe.submit(time.sleep, 1) |
The call does not block, it returns immediately.
Notice that we specified the name of the function and did not call the time.sleep() function as this would not execute.
For example, this would be a bug and would not have the desired effect:
1 2 3 |
... # issue the sleep function as a task (bug) future = tpe.submit(time.sleep(), 1) |
Also notice that we provide the arguments to the target function as arguments directly to the submit() method.
If we issue a task using the submit() method that has many arguments, these are provided sequentially as arguments to the submit() method.
For example:
1 2 3 |
... # issue a task with many arguments future = tpe.submit(task, arg1, arg2, arg3) |
The Future object provides a handle on the task.
We can use it to check the status of the tasks, such as running(), done(), or cancelled().
For example, we can check if the task is done (finished normally or failed with an exception) or not via the done() method:
1 2 3 4 |
... # check if the task is done if future.done(): # ... |
We can also use it to get the return value result of the task via the result() method or the unhandled exception raised by the task via the exception() method. These methods are blocking calls and will return when the result or exception is available.
For example, we can get the return value result of the task via the result() method:
1 2 3 |
... # block and get the result once it is available data = future.result() |
We can also add a callback function to the Future object via the add_done_callback() method. The callback is then called automatically for us once the task is done.
You can learn more about the Future object n the tutorial:
It is common to issue many calls to the same target function with different arguments using the submit() method.
This can be performed in a list comprehension so that the Future object for each task is collected into a list. This list of Future objects can then be used with module functions such as as_completed() and wait().
For example:
1 2 3 |
... # issue many tasks in a list comprehension futures = [tpe.submit(task, i) for i in range(100)] |
You can learn more about common usage patterns in the tutorial:
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.
Common Questions About ThreadPoolExecutor submit()
This section explores some common questions about the ThreadPoolExecutor submit() method.
What Are The Arguments to submit()?
The submit() method takes the name of the target function to execute and any arguments to the target function, either as positional arguments or as named arguments.
What Does submit() Return?
The submit() method returns a Future object which provides a handle on the issued task.
Does The submit() method Block?
No, the submit() method does not block and will return immediately.
How Can We Wait for a submit() Task To Complete?
We can wait for a submitted task to complete by calling the result() method on the returned future.
For example:
1 2 3 |
... # wait for task to be done _ = future.result() |
If the target function does not return a value, then result() will return None.
How Can We Get the Return Value From The Target Function?
We can get the return value from the target function issued via submit() using the result() method on the Future object.
For example:
1 2 3 |
... # block and get the result once it is available data = future.result() |
You can learn more in the tutorial:
How Can We Get The Exception Raised By The Target Function?
We can get any exception raised by the target function issued to submit() via the call to exception() on the Future object.
For example:
1 2 3 |
... # block and wait for any exception raise by the task exp = future.exception() |
If the target function did not raise an exception, then a None value is returned.
You can learn more in the tutorial:
How Can We Check On The Status Of a Task Issued Via submit()?
We can check on the status of a task issued via submit via the returned Future object.
We can use methods such as done(), running(), and cancelled() that return boolean values.
For example:
1 2 3 4 |
... # check of the task is running if future.running(): # ... |
You can learn more about how to check task status in the tutorial:
How Can We Add a Done Callback To a Task Issued Via submit()?
A done callback is a custom function that takes one argument, the future that represents the task.
The callback function is executed once the task is completed, either normally or via an exception.
We can add a done callback to a task issued via submit() via the add_done_callback() function on the Future object for the task.
For example:
1 2 3 4 5 6 7 |
# custom callback function def callback(future): # ... ... # add done callback future.add_done_callback(callback) |
You can learn more in the tutorial:
Can We Issue Other Function Types to submit()?
Yes.
We can issue a range of function types to the submit() method, including:
- pure functions
- user-defined functions
- custom functions
- anonymous lambda functions
- object instance methods
- static methods
You can learn more and see examples in the tutorial:
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
ThreadPoolExecutor submit() vs map()
The ThreadPoolExecutor map() method takes a function name and an iterable of arguments. It then issues one task per item in the iterable to the ThreadPoolExecutor and returns an iterable of return values.
For example:
1 2 3 4 5 6 7 |
... # start the thread pool with ThreadPoolExecutor() as tpe: # execute tasks concurrently and process results in order for result in tpe.map(task, range(10)): # report the result print(result) |
Although the tasks are executed asynchronously when issued via map(), the results are iterated in the order of the iterable provided to the map() method.
We can think of the ThreadPoolExecutor version of the map() function as an asynchronous version of the built-in map() function and is ideal if you are looking to update your for loop to use threads.
You can learn more about the map() method in the tutorial:
Let’s compare the map() and submit() functions for the ThreadPoolExecutor.
Both the map() and submit() functions are similar in that they both allow you to execute tasks asynchronously using threads.
The map() function is simpler:
- It is a threaded version of the built-in map() function.
- It assumes you want to call the same function many times with different values.
- It only takes iterables as arguments to the target function.
- It only allows you to iterate results from the target function.
In an effort to keep your code simpler and easier to read, you should try to use map() first, before you try to use the submit() function.
The simplicity of the map() function means it is also limited:
- It does not provide control over the order that task results are used.
- It does not provide a way to check the status of tasks.
- It does not allow you to cancel tasks before they start running.
- It does not allow you control over how to handle an exception raised by a task function.
If the map() function is too restrictive, you may want to consider the submit() function instead.
The submit() function gives more control:
- It assumes you want to submit one task at a time.
- It allows a different target function with a variable number of arguments for each task.
- It allows you to check on the status of each task.
- It allows you to cancel a task before it has started running.
- It allows callback functions to be called automatically when tasks are done.
- It allows you to handle an exception raised by a target task function.
- It allows you control over when you would like to get the result of the task, if at all.
- It can be used with module functions like wait() and as_completed() to work with tasks in groups.
The added control with submit() comes with added complexity:
- It requires that you manage the Future object for each task.
- It requires that you explicitly retrieve the result for each task.
- It requires extra code if you need to apply the same function with different arguments.
You can learn more about how submit() compares to map() in the tutorial:
Now that we are familiar with the submit() method, let’s explore some worked examples.
Example of submit() Function With No Arguments
We can explore an example of using the submit() method with a target function that takes no arguments and has no return value.
We can define a task function that reports a message, sleeps a moment to simulate doing some work, then reports a final message.
The task() method below implements this.
1 2 3 4 5 6 7 8 |
# task executed in the thread pool def task(): # report a message print('Task running') # block for a moment sleep(1) # report a message print('Task done') |
We can then issue one task to the ThreadPoolExecutor that executes our task() function and then wait for the task to complete by calling the result() method on the returned Future. We can ignore the result because our function does not return value.
1 2 3 4 5 6 7 |
... # create the thread pool with ThreadPoolExecutor() as tpe: # issue the task future = tpe.submit(task) # wait for the task to complete _ = future.result() |
Tying this together, the complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# SuperFastPython.com # example threadpoolexecutor submit() with no arguments from concurrent.futures import ThreadPoolExecutor from time import sleep # task executed in the thread pool def task(): # report a message print('Task running') # block for a moment sleep(1) # report a message print('Task done') # create the thread pool with ThreadPoolExecutor() as tpe: # issue the task future = tpe.submit(task) # wait for the task to complete _ = future.result() |
Running the example first creates a ThreadPoolExecutor with a default number of threads using the context manager interface.
Next, the task() function is issued to the ThreadPoolExecutor and a Future object that represents the task is returned.
The main thread then calls the result() method to get the result from the task, blocking until the task is complete.
The task() function is executed by a worker in the ThreadPoolExecutor. A message is reported, then the task sleeps for a moment. The task then resumes and reports a final message.
The task completes and the main thread resumes.
The context manager is exited and the ThreadPoolExecutor is shut down, releasing all worker threads.
This highlights how we can issue a task with no arguments or return values to the ThreadPoolExecutor using the submit() method.
1 2 |
Task running Task done |
Example of submit() Function With One Argument
We can explore an example of using the submit() method with a target function that takes a single argument and has no return value.
We can update the above example and change the task() function to take one argument.
In this case, the argument is the duration for the task to sleep in seconds, which is then used in the call to the time.sleep() function.
The updated task() function with these changes is listed below.
1 2 3 4 5 6 7 8 |
# task executed in the thread pool def task(sleep_time): # report a message print('Task running') # block for a moment sleep(sleep_time) # report a message print('Task done') |
We can then call the submit() method on the ThreadPoolExecutor and issue the task() method with an argument.
1 2 3 |
... # issue the task future = tpe.submit(task, 0.5) |
Tying this together, the complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# SuperFastPython.com # example threadpoolexecutor submit() with one argument from concurrent.futures import ThreadPoolExecutor from time import sleep # task executed in the thread pool def task(sleep_time): # report a message print('Task running') # block for a moment sleep(sleep_time) # report a message print('Task done') # create the thread pool with ThreadPoolExecutor() as tpe: # issue the task future = tpe.submit(task, 0.5) # wait for the task to complete _ = future.result() |
Running the example first creates a ThreadPoolExecutor with a default number of threads using the context manager interface.
Next, the task() function is issued to the ThreadPoolExecutor with a single argument, and a Future object that represents the task is returned.
The main thread then calls the result() method to get the result from the task, blocking until the task is complete.
The task() function is executed by a worker in the ThreadPoolExecutor. A message is reported, then the task sleeps for a moment using the argument passed into the function. The task then resumes and reports a final message.
The task completes and the main thread resumes.
The context manager is exited and the ThreadPoolExecutor is shut down, releasing all worker threads.
This highlights how we can issue a task with one argument and no return values to the ThreadPoolExecutor using the submit() method.
1 2 |
Task running Task done |
Example of submit() Function With Many Arguments
We can explore an example of using the submit() method with a target function that takes multiple arguments and has no return value.
We can update the above example and change the task() function to take multiple arguments.
In this case, the task() function will take a duration in seconds that the function will sleep and a message to report in the print() statements.
The updated task() function with these changes is listed below.
1 2 3 4 5 6 7 8 |
# task executed in the thread pool def task(sleep_time, message): # report a message print(f'Task running: {message}') # block for a moment sleep(sleep_time) # report a message print(f'Task done: {message}') |
We can then issue the task() function to the ThreadPoolExecutor using the submit() method and specify the two arguments to the task() function.
1 2 3 |
... # issue the task future = tpe.submit(task, 0.5, 'Hello!') |
Tying this together, the complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# SuperFastPython.com # example threadpoolexecutor submit() with many arguments from concurrent.futures import ThreadPoolExecutor from time import sleep # task executed in the thread pool def task(sleep_time, message): # report a message print(f'Task running: {message}') # block for a moment sleep(sleep_time) # report a message print(f'Task done: {message}') # create the thread pool with ThreadPoolExecutor() as tpe: # issue the task future = tpe.submit(task, 0.5, 'Hello!') # wait for the task to complete _ = future.result() |
Running the example first creates a ThreadPoolExecutor with a default number of threads using the context manager interface.
Next, the task() function issued to the ThreadPoolExecutor with two arguments, a number and a string, and a Future object that represents the task is returned.
The main thread then calls the result() method to get the result from the task, blocking until the task is complete.
The task() function is executed by a worker in the ThreadPoolExecutor. A message is reported using the provided string argument, then the task sleeps for a moment using the argument passed into the function. The task then resumes and reports a final message using the string argument.
The task completes and the main thread resumes.
The context manager is exited and the ThreadPoolExecutor is shutdown, releasing all worker threads.
This highlights how we can issue a task with multiple arguments and no return values to the ThreadPoolExecutor using the submit() method.
1 2 |
Task running: Hello! Task done: Hello! |
Example of submit() With Return Value
We can explore an example of using the submit() method with a target that takes no arguments but returns a value.
In this example, we can update the above example so that the task() function generates a random value between 0 and 1, sleeps for this fraction of a second, then returns the generated value.
The updated task() function with these changes is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 |
# task executed in the thread pool def task(): # report a message print('Task running') # generate a random value between 0 and 1 value = random() # block for a moment sleep(value) # report a message print('Task done') # return generated value return value |
We can then issue the task() function using the submit() method as before, then retrieve the result from the returned Future object, once it is available, and report the return value from the task function.
1 2 3 4 5 6 7 |
... # issue the task future = tpe.submit(task) # wait for the task to complete result = future.result() # report result print(f'Result: {result}') |
Tying this together, the complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# SuperFastPython.com # example threadpoolexecutor submit() with return value from concurrent.futures import ThreadPoolExecutor from time import sleep from random import random # task executed in the thread pool def task(): # report a message print('Task running') # generate a random value between 0 and 1 value = random() # block for a moment sleep(value) # report a message print('Task done') # return generated value return value # create the thread pool with ThreadPoolExecutor() as tpe: # issue the task future = tpe.submit(task) # wait for the task to complete result = future.result() # report result print(f'Result: {result}') |
Running the example first creates a ThreadPoolExecutor with a default number of threads using the context manager interface.
Next, the task() function is issued to the ThreadPoolExecutor with no arguments, and a Future object that represents the task is returned.
The main thread then calls the result() method to get the result from the task, blocking until the task is complete.
The task() function is executed by a worker in the ThreadPoolExecutor. A message is reported, a random number is generated then the task sleeps for a fraction of a second. The task then resumes and reports a final message and the random number that was generated is returned.
The task completes and the main thread resumes. The return value is retrieved from the Future object and reported.
The context manager is exited and the ThreadPoolExecutor is shut down, releasing all worker threads.
This highlights how we can issue a task with no arguments to the ThreadPoolExecutor using the submit() method and retrieve and report the return value using the Future object.
1 2 3 |
Task running Task done Result: 0.7270795495752382 |
Example of submit() With Exception
We can explore an example of using the submit() method with a target that takes no arguments but fails with an unhandled exception.
In this example, we can update the above example so that the task() function reports a message and sleeps as per normal, then raises an exception.
The updated task() function with these changes is listed below.
1 2 3 4 5 6 7 8 |
# task executed in the thread pool def task(): # report a message print('Task running') # block for a moment sleep(1) # fail with an exception raise Exception('Something bad happened') |
The task is issued using the submit() method as per normal. We can then retrieve the exception from the Future object using the exception() method and report its details.
1 2 3 4 5 6 7 |
... # issue the task future = tpe.submit(task) # wait for the task to complete, get exception exp = future.exception() # report the exception print(f'Failed: {exp}') |
Tying this together, the complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# SuperFastPython.com # example threadpoolexecutor submit() with exception from concurrent.futures import ThreadPoolExecutor from time import sleep # task executed in the thread pool def task(): # report a message print('Task running') # block for a moment sleep(1) # fail with an exception raise Exception('Something bad happened') # create the thread pool with ThreadPoolExecutor() as tpe: # issue the task future = tpe.submit(task) # wait for the task to complete, get exception exp = future.exception() # report the exception print(f'Failed: {exp}') |
Running the example first creates a ThreadPoolExecutor with a default number of threads using the context manager interface.
Next, the task() function is issued to the ThreadPoolExecutor with no arguments, and a Future object that represents the task is returned.
The main thread then calls the exception() method to get the exception raised by the task, blocking it until the task is done.
The task() function is executed by a worker in the ThreadPoolExecutor. A message is reported, a random number is generated then the task sleeps for a fraction of a second. The task then resumes and raises an exception. This causes the task to fail and be done.
The main thread resumes and retrieves the exception from the task via the Future object. The details of the exception are then reported.
The context manager is exited and the ThreadPoolExecutor is shut down, releasing all worker threads.
This highlights how we can issue a task with no arguments to the ThreadPoolExecutor using the submit() method that fails with an unhandled exception and how we can explicitly retrieve and report the exception that was raised.
1 2 |
Task running Failed: Something bad happened |
Example of submit() And Not Wait For Task To Complete
We can explore an example of using the submit() method with a target function that takes no arguments and has no return value and does not explicitly wait for the task to complete.
This can be achieved by using the task using the submit() method as per normal and then not explicitly waiting for a return value via the result() method in the Future or waiting for an exception via the exception() method.
Instead, we will rely on the shutdown() method to wait for all tasks to complete, which is called automatically when we exit the context manager for the ThreadPoolExecutor.
The complete example of this change is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# SuperFastPython.com # example threadpoolexecutor submit() and not waiting from concurrent.futures import ThreadPoolExecutor from time import sleep # task executed in the thread pool def task(): # report a message print('Task running') # block for a moment sleep(1) # report a message print('Task done') # create the thread pool with ThreadPoolExecutor() as tpe: # issue the task _ = tpe.submit(task) |
Running the example first creates a ThreadPoolExecutor with a default number of threads using the context manager interface.
Next, the task() function is issued to the ThreadPoolExecutor, and a Future object that represents the task is returned.
The main thread exits the ThreadPoolExecutor context manager. This causes the shutdown() method to be called automatically and blocks the main thread until all tasks in the ThreadPoolExecutor are complete and all worker threads have been terminated.
The task() function is executed by a worker in the ThreadPoolExecutor. A message is reported, then the task sleeps for a moment. The task then resumes and reports a final message.
The task completes and the main thread resumes and the program exits.
This highlights how we can issue tasks to the ThreadPoolExecutor using the submit() method and not explicitly wait for them to complete.
1 2 |
Task running Task done |
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
Takeaways
You now know how to use the ThreadPoolExecutor submit() method.
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Photo by Stefano Ghezzi on Unsplash
Do you have any questions?