How to use ThreadPoolExecutor submit()

August 3, 2023 Python ThreadPoolExecutor

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:

...
# 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:

...
# 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:

...
# 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.

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:

...
# 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:

...
# 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:

...
# 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:

...
# 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:

...
# 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:

...
# 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:

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:

...
# 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:

...
# 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:

...
# 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:

...
# 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:

# 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:

You can learn more and see examples in the tutorial:

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:

...
# 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:

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:

If the map() function is too restrictive, you may want to consider the submit() function instead.

The submit() function gives more control:

The added control with submit() comes with added complexity:

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.

# 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.

...
# 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.

# 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.

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.

# 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.

...
# issue the task
future = tpe.submit(task, 0.5)

Tying this together, the complete example is listed below.

# 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.

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.

# 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.

...
# issue the task
future = tpe.submit(task, 0.5, 'Hello!')

Tying this together, the complete example is listed below.

# 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.

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.

# 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.

...
# 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.

# 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.

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.

# 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.

...
# 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.

# 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.

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.

# 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.

Task running
Task done

Takeaways

You now know how to use the ThreadPoolExecutor submit() method.



If you enjoyed this tutorial, you will love my book: Python ThreadPoolExecutor Jump-Start. It covers everything you need to master the topic with hands-on examples and clear explanations.