How to Cancel Tasks with the ThreadPoolExecutor in Python

November 11, 2021 Python ThreadPoolExecutor

You can cancel tasks in the ThreadPoolExecutor by calling the cancel() function on the Future object.

In this tutorial you will discover how to cancel tasks in a Python thread pool.

Let's get started.

Unwanted Task Submitted to the ThreadPoolExecutor

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 that allows you to check on the status of the task and get the result from the task once it completes.

You may submit tasks that you may later decide you no longer need to complete.

Letting the tasks run may use up unwanted resources.

Additionally, you may have to wait for all tasks in the thread pool to complete before you can shut it down.

The solution is to cancel unwanted tasks.

Call cancel() on the Future to Cancel a Task

You can cancel tasks submitted to the ThreadPoolExecutor by calling the cancel() function on the Future object.

Recall that you will receive a Future object when you submit your task to the thread pool by calling the submit() function.

...
# submit a task to the thread pool
future = executor.submit(work)

The Future object has a function called cancel() that will cancel the task if it has not yet started running and is not done.

...
# cancel a task submitted to the thread pool
was_canceled = future.cancel()

The cancel() function returns True if the task was canceled, otherwise False if the task could not be canceled.

If you successfully cancel the task, it will not execute.

If you try to retrieve a result from a cancelled task by calling the result() function, a CancelledError will be raised. Similarly, if you try to get any exception raised while executing the task that was cancelled by calling the exception() function, a CancelledError will be raised.

For example:

...
# get the result
try:
    result = future.result()
except CancelledError:
    print('No result, task was cancelled')

You may fail to cancel a task submitted to the thread pool for one of three reasons:

There is often some confusion with the difference between a task that has been submitted and a task that is running.

A ThreadPoolExecutor has a fixed number of threads, limiting the number of tasks that can be executed at once.

A task that is submitted to the ThreadPoolExecutor may not start running immediately if all threads in the pool are currently occupied. Once a thread in the pool becomes available, it will take a task from the internal queue of tasks and begin executing it, changing its status to running.

We can check if a task is running by calling the running() function on the Future object for the task.

Only tasks that are scheduled in the thread pool but have not yet started running can be cancelled.

Now that we know how to cancel tasks in a ThreadPoolExecutor, let's look at a worked example.

How To Cancel a Task in the ThreadPoolExecutor

Let's demonstrate how to cancel a task submitted to the ThreadPoolExecutor.

First, we can define a simple task that will sleep for a given number of seconds.

# mock task that will sleep for a moment
def work(sleep_time):
    sleep(sleep_time)

We can then create a thread pool with one thread and submit a long running task to occupy the single thread in the pool.

...
# create a thread pool
with ThreadPoolExecutor(1) as executor:
    # start a long running task
    future1 = executor.submit(work, 2)
    print(f'First task running={future1.running()}')

Next, we can submit a second shorter task and confirm that it is not yet running.

...
# start a second
future2 = executor.submit(work, 0.1)
print(f'Second task running={future2.running()}')

We can then cancel the second task, and confirm that it indeed was canceled and will not run.

...
# cancel the second task
was_cancelled = future2.cancel()
print(f'Second task was cancelled: {was_cancelled}')

Tying this together, the complete example of cancelling a task in the ThreadPoolExecutor is listed below.

# SuperFastPython.com
# example of cancelling a task in a thread pool
from time import sleep
from concurrent.futures import ThreadPoolExecutor

# mock task that will sleep for a moment
def work(sleep_time):
    sleep(sleep_time)

# create a thread pool
with ThreadPoolExecutor(1) as executor:
    # start a long running task
    future1 = executor.submit(work, 2)
    print(f'First task running={future1.running()}')
    # start a second
    future2 = executor.submit(work, 0.1)
    print(f'Second task running={future2.running()}')
    # cancel the second task
    was_cancelled = future2.cancel()
    print(f'Second task was cancelled: {was_cancelled}')

Running the example first starts the long running task and confirms that it is running.

The second short duration task is submitted and is not yet running.

We then cancel the second task and confirm that it will not run.

First task running=True
Second task running=False
Second task was cancelled: True

No Results or Exception from Cancelled Tasks

You cannot get results or exceptions from cancelled tasks.

If we try to get a result from a cancelled task by calling the result() function, a CancelledError would be raised. The same would be the case if we tried to get the exception by calling exception().

We can demonstrate this by updating the example from the previous section.

First, let's update the target task function to return a value so we have a result to retrieve from the task. In this case, the argument that was passed in.

# mock task that will sleep for a moment
def work(sleep_time):
    sleep(sleep_time)
    return sleep_time

Next, we can attempt to get the result from the cancelled task (the short duration task) and handle the expected CancelledError that will be raised because the task was cancelled.

...
# get the result
try:
    result = future2.result()
except CancelledError:
    print('No result, task was cancelled')

Finally, we can try and get the exception from the cancelled task and handle the same expected CancelledError because the task was cancelled.

...
# get the exception
try:
    e = future2.exception()
except CancelledError:
    print('No exception, task was cancelled')

Tying this together, the complete example of attempting to get a result and an exception from a cancelled task is listed below.

# SuperFastPython.com
# example of getting results and an exception from a cancelled task
from time import sleep
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import CancelledError

# mock task that will sleep for a moment
def work(sleep_time):
    sleep(sleep_time)
    return sleep_time

# create a thread pool
with ThreadPoolExecutor(1) as executor:
    # start a long running task
    future1 = executor.submit(work, 2)
    print(f'First task running={future1.running()}')
    # start a second
    future2 = executor.submit(work, 0.1)
    print(f'Second task running={future2.running()}')
    # cancel the second task
    was_cancelled = future2.cancel()
    print(f'Second task was cancelled: {was_cancelled}')
    # get the result
    try:
        result = future2.result()
    except CancelledError:
        print('No result, task was cancelled')
    # get the exception
    try:
        e = future2.exception()
    except CancelledError:
        print('No exception, task was cancelled')

Running the example first starts the long running task and confirms that it is running. The second short duration task is submitted, then successfully cancelled.

We then try to retrieve the result from the cancelled task and handle an expected CancelledError.

Finally, we try to retrieve any exception that occurred during the execution of the short duration task and a CancelledError is raised and handled, again because the task was cancelled.

First task running=True
Second task running=False
Second task was cancelled: True
No result, task was cancelled
No exception, task was cancelled

Takeaways

You now know how to cancel tasks that have been submitted to the Python ThreadPoolExecutor.



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.