ThreadPoolExecutor Kill Running Tasks
You can kill running tasks in the ThreadPoolExecutor by running the ThreadPoolExecutor in a new child process and calling the terminate() or kill() method on the child process.
This will abruptly and immediately terminate the child process and terminate all worker threads running in the ThreadPoolExecutor.
In this tutorial, you will discover how to kill running tasks in the ThreadPoolExecutor.
Let's get started.
shutdown() Won't Stop Running Tasks in 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 or via the context manager interface and tasks can be issued one-by-one via the submit() method or in batch via the map() method.
For example:
...
# create a thread pool
with ThreadPoolExecutor() as tpe:
# issue a task
future = type.submit(task)
# the the task result once the task is done
result = future.result()
You can learn more about the ThreadPoolExecutor in the tutorial:
The ThreadPoolExecutor can be shut down by calling the shutdown() method.
For example:
...
# shutdown the thread pool
tpe.shutdown()
This call can be configured to block until all tasks are done or return immediately and cancel all pending tasks or not.
You can learn more about how to shut down the ThreadPoolExecutor in the tutorial:
Shutting down the ThreadPoolExecutor will not terminate running tasks.
It is possible to update tasks to check a shared variable and terminate of their own accord. You can see an example of this in the tutorial:
Nevertheless, we cannot immediately terminate tasks running in the ThreadPoolExecutor.
Even if the program exists, the Python interpreter will wait for all threads in the ThreadPoolExecutor to complete and be released before the program terminates.
How can we forcefully terminate all running tasks in the ThreadPoolExecutor?
How to Kill All Running Tasks in the ThreadPoolExecutor
There are a few ways that we may implement the forceful termination of worker threads in the ThreadPoolExecutor.
An approach I prefer is to host the ThreadPoolExecutor in a separate child process, then terminate the entire process in order to forcefully stop all running tasks.
For example, we can define a new function that creates and manages the ThreadPoolExecutor.
# function for managing a new thread pool
def tpe_runner():
# create the thread pool
with ThreadPoolExecutor(2) as tpe:
# issue tasks
# ...
We can then create a new multiprocessing.Process instance to run the function in a child process.
For example:
...
# start a child process to run the thread pool
process = Process(target=tpe_runner)
process.start()
You can learn more about running functions in a child process in the tutorial:
Finally, when we want to stop all tasks immediately, we can call the terminate() method on the process.
For example:
...
# stop all threads in the thread pool
process.terminate()
You can learn more about terminating and killing child processes in the tutorial:
This is a clean solution in that it does not require that the ThreadPoolExecutor class or the Future class be customized with workers or tasks that can be killed.
Instead, it provides a single point at which all workers and all helper threads in the ThreadPoolExecutor can be killed immediately.
The downside is that the ThreadPoolExecutor is not recoverable. It is terminated along with the workers. It also has the downside that any data needed by workers or any results produced by workers must be transferred from to or from the new child process, which can be costly if there is a lot of data involved.
Another downside is that tasks may be terminated mid-operation. This means that data or program state may be left inconsistent.
Now that we know how to forcefully terminate all workers in the ThreadPoolExecutor, let's look at a worked example.
Example of Killing All Running Tasks in the ThreadPoolExecutor
We can develop an example that explores how to forcefully stop all worker threads in the ThreadPoolExecutor.
In this example, we will define a simple task to execute many times in our thread pool. We will then define a function that will be used to manage the thread pool in a new process. Finally, the main process will create a new child process to manage the thread pool, wait a while, then terminate the child process along with all running worker threads.
Firstly, we can define a task to execute in the thread pool.
The task tasks a unique integer argument sleeps for a moment to simulate doing work, then reports a message with its unique identifier.
These print messages will give us some idea that tasks are running and being completed in the thread pool.
The task() function below implements this.
# task executed in a thread pool
def task(identifier):
# block for a moment to simulate effort
sleep(1)
# report a message
print(f'Task {identifier} done.', flush=True)
Next, we can define a function to manage the ThreadPoolExecutor and issue all tasks, which will be executed in a child process.
The function will report a message at the beginning and end so we have some idea if the function is running and finished normally.
A ThreadPoolExecutor is created normally using the context manager interface with two worker threads. Then 10 task() calls are issued to the pool with unique arguments from 0 to 9.
The context manager interface ensures that the pool is closed automatically if the block is exited, blocking until all tasks are completed.
The tpe_runner() function below implements this.
# function for managing a new thread pool
def tpe_runner():
# report a message
print('TPE Runner has started.', flush=True)
# create the thread pool
with ThreadPoolExecutor(2) as tpe:
# issue a bunch of tasks
_ = tpe.map(task, range(10))
# block and wait for tasks to be done
print('TPE Runner is done.', flush=True)
Finally, we can develop the code in the main process.
First, we will create a new process to run our tpe_runner() function, then start this process.
...
# start a child process to run the thread pool
process = Process(target=tpe_runner)
process.start()
Next, the main process will block moments to allow some tasks to run and complete.
...
# wait a while
print('Main thread is waiting a moment...')
sleep(3)
Finally, the main process terminates the child process running the ThreadPoolExecutor.
...
# main is terminating the tpe process
print('Main is terminating the tpe...')
process.terminate()
print('Main is done.')
Tying this together, the complete example is listed below.
# SuperFastPython.com
# example of killing a threadpoolexecutor
from multiprocessing import Process
from concurrent.futures import ThreadPoolExecutor
from time import sleep
# task executed in a thread pool
def task(identifier):
# block for a moment to simulate effort
sleep(1)
# report a message
print(f'Task {identifier} done.', flush=True)
# function for managing a new thread pool
def tpe_runner():
# report a message
print('TPE Runner has started.', flush=True)
# create the thread pool
with ThreadPoolExecutor(2) as tpe:
# issue a bunch of tasks
_ = tpe.map(task, range(10))
# block and wait for tasks to be done
print('TPE Runner is done.', flush=True)
# protect the entry point
if __name__ == '__main__':
# start a child process to run the thread pool
process = Process(target=tpe_runner)
process.start()
# wait a while
print('Main thread is waiting a moment...')
sleep(3)
# main is terminating the tpe process
print('Main is terminating the tpe...')
process.terminate()
print('Main is done.')
Running the example first creates a new child process to run our tpe_runner() function. Then the new child process is started.
Next, the main process reports a message and blocks with a sleep.
The child process runs, reports a message then creates the ThreadPoolExecutor. All 10 task() calls are issued to the ThreadPoolExecutor and the child process until all tasks are complete.
The two worker threads in the ThreadPoolExecutor run and execute task() functions with unique arguments.
Two tasks are completed, then two more.
The main thread resumes and terminates the child process.
This immediately stops the child process and in turn, it terminates the ThreadPoolExecutor. This includes the termination of the two running worker threads and any helper threads internal to the ThreadPoolExecutor.
A final message is reported and the program terminates, again, not waiting for the workers in the child process to terminate.
This highlights how we can immediately terminate running tasks in the ThreadPoolExecutor.
Main thread is waiting a moment...
TPE Runner has started.
Task 0 done.
Task 1 done.
Task 2 done.
Task 3 done.
Main is terminating the tpe...
Main is done.
Takeaways
You now know how to kill running tasks in the 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.