Last Updated on October 29, 2022
You cannot terminate worker threads in the ThreadPool class.
In this tutorial you will discover the effect of the terminate() method on the ThreadPool in Python.
Let’s get started.
What is the ThreadPool
The multiprocessing.pool.ThreadPool in Python provides a pool of reusable threads for executing ad hoc tasks.
A thread pool object which controls a pool of worker threads to which jobs can be submitted.
— multiprocessing — Process-based parallelism
The ThreadPool class extends the Pool class. The Pool class provides a pool of worker processes for process-based concurrency.
Although the ThreadPool class is in the multiprocessing module it offers thread-based concurrency and is best suited to IO-bound tasks, such as reading or writing from sockets or files.
A ThreadPool can be configured when it is created, which will prepare the new threads.
We can issue one-off tasks to the ThreadPool using methods such as apply() or we can apply the same function to an iterable of items using methods such as map().
Results for issued tasks can then be retrieved synchronously, or we can retrieve the result of tasks later by using asynchronous versions of the methods such as apply_async() and map_async().
Run loops using all CPUs, download your FREE book to learn how.
What is terminate()
Processes in Python can be terminated.
This can be achieved via the terminate() method on the multiprocessing.Process class.
For example:
1 2 3 |
... # terminate a process process.terminate() |
Calling terminate will raise a SIGTERM signal in the target process which if not handled by the process will result in the process terminating (nearly) immediately.
terminate(): Terminate the process. On Unix this is done using the SIGTERM signal; on Windows TerminateProcess() is used. Note that exit handlers and finally clauses, etc., will not be executed.
— multiprocessing — Process-based parallelism
Similarly the multiprocessing pool of worker processes can be terminated by calling the terminate() method on the multiprocessing.Pool class.
For example:
1 2 3 |
... # terminate the process pool pool.terminate() |
This will call the terminate() method on the worker processes in the pool, even if they are currently executing a task.
terminate(): Stops the worker processes immediately without completing outstanding work. When the pool object is garbage collected terminate() will be called immediately.
— multiprocessing — Process-based parallelism
The terminate() method on the Pool class is also called automatically when using the Pool via the context manager interface.
For example:
1 2 3 4 5 |
... # create a pool via the context manager with Pool() as pool: #... # terminate() is called automatically |
The multiprocessing.pool.ThreadPool class extends the Pool class and has a terminate() method that can be called.
For example:
1 2 3 |
... # terminate the thread pool pool.terminate() |
There’s just one problem, it does nothing.
Threads Cannot Be Terminated Directly
Threads cannot be terminated.
The threading.Thread class does not have a terminate() method.
As such, we cannot directly terminate or stop a thread in Python.
Instead, we must use indirect methods, such as having the thread check a variable to choose to stop.
You can learn more about how to gracefully stop a thread in the tutorial:
This has an impact on the ThreadPool class and calling the terminate() method.
Free Python ThreadPool Course
Download your FREE ThreadPool PDF cheat sheet and get BONUS access to my free 7-day crash course on the ThreadPool API.
Discover how to use the ThreadPool including how to configure the number of worker threads and how to execute tasks asynchronously
ThreadPool Does Not Support Terminate
Because threads do not support the terminate() method, the ThreadPool terminate() method does not work as expected.
At the latest at the time of writing in Python version 3.10 and lower.
As mentioned, the terminate method exists and can be called.
We can learn more if we check the source code for the Pool class which the ThreadPool class extends.
For example, the snippet below is called when the terminate() method is called directly or indirectly via the context manager interface:
1 2 3 4 5 6 7 |
... # Terminate workers which haven't already finished. if pool and hasattr(pool[0], 'terminate'): util.debug('terminating workers') for p in pool: if p.exitcode is None: p.terminate() |
It checks that the unit of concurrency, a process or thread, supports the terminate() method, and if so, calls it.
Therefore, when we call terminate() in a ThreadPool object, it does not terminate the threads.
Instead, it closes the threads that are not running, and waits for the executing threads to complete.
Calling terminate() on the ThreadPool has the same effect as calling the close() method.
close(): Prevents any more tasks from being submitted to the pool. Once all the tasks have been completed the worker processes will exit.
— multiprocessing — Process-based parallelism
This is not a bug per se, it is just an undocumented feature.
In fact, the API documentation for the ThreadPool class at the time of writing is practically non-existent. Which is why I am writing all of these tutorials.
We can demonstrate this with a worked example.
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
Example of ThreadPool Not Terminating
In this example we can demonstrate that calling terminate() on a ThreadPool does not terminate tasks that are running.
In this example we will first create a ThreadPool. We will then issue a task that blocks for a long time.
The main process will then report all the threads that are running in the process, e.g. all the workers and all the helpers.
It will then terminate the ThreadPool by calling the terminate() method. It then reports all the running threads in the process again, to confirm non-running threads were closed and running threads are still running.
This will highlight that terminate() does not have the desired effect and operates like calling close().
Firstly, let’s define a task that blocks for 10 seconds then reports a message when it is done.
This is helpful because if the task is not terminated and is instead closed, we will see the message. Whereas if the task is truly terminated, we will not see the message
The task() function below implements this.
1 2 3 4 5 6 |
# task function executed in a new thread def task(): # block for a moment sleep(10) # display a message print('This is coming from another thread') |
Next, in the main thread, we will create a ThreadPool.
1 2 3 |
... # create a thread pool pool = ThreadPool() |
We will then issue a single task asynchronously into the pool, in this case via the apply_async() method.
1 2 3 |
... # execute the task asynchronously _ = pool.apply_async(task) |
The main thread will then report all of the threads running in the process.
This is for reference after we call terminate() so we can see what threads were stopped and what threads remain running.
1 2 3 4 5 |
... # report all threads print('main reporting all threads:') for thread in threading.enumerate(): print(f'\t{thread}') |
Next, the main thread will wait a moment, then terminate the ThreadPool by calling the terminate() method.
1 2 3 4 5 6 |
... print('Main is waiting a moment...') sleep(2) # terminate the pool print('Main is terminating the pool') pool.terminate() |
The main thread then waits a moment more, then reports all of the threads still running in the process, after terminate() was called.
We expect that most workers and helper thread have closed and that the single worker executing our task remains running.
1 2 3 4 5 6 7 8 |
... # wait a moment print('Main is waiting a moment...') sleep(1) # check if the pool is terminated print('Main reporting all threads:') for thread in threading.enumerate(): print(f'\t{thread}') |
Finally, the main thread joins the ThreadPool and waits for it to close completely. That is, the main thread blocks until all workers have closed.
1 2 3 4 5 |
... # main joining the thread pool print('Main joining the pool...') pool.join() print('Main done.') |
You can learn more about joining the ThreadPool in the tutorial:
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 28 29 30 31 32 33 34 35 36 37 38 39 40 |
# SuperFastPython.com # example of thread workers not being terminated from time import sleep from multiprocessing.pool import ThreadPool import threading # task function executed in a new thread def task(): # block for a moment sleep(10) # display a message print('This is coming from another thread') # protect the entry point if __name__ == '__main__': # create a thread pool pool = ThreadPool() # execute the task asynchronously _ = pool.apply_async(task) # report all threads print('main reporting all threads:') for thread in threading.enumerate(): print(f'\t{thread}') # wait a moment print('Main is waiting a moment...') sleep(2) # terminate the pool print('Main is terminating the pool') pool.terminate() # wait a moment print('Main is waiting a moment...') sleep(1) # check if the pool is terminated print('Main reporting all threads:') for thread in threading.enumerate(): print(f'\t{thread}') # main joining the thread pool print('Main joining the pool...') pool.join() print('Main done.') |
Running the example first starts the ThreadPool and issues the task.
The task starts running in a worker within the ThreadPool and blocks for 10 seconds.
The main thread reports all threads running in the process.
In this case, we can see the main thread, 8 worker threads, and 3 helper threads.
Note, the number of worker threads will differ depending on the number of logical CPU cores you have in your system.
The main thread then waits a moment, then terminates the ThreadPool and waits a moment more.
It then reports all running threads in the process.
This time we can only see the main thread and one worker thread. This is the worker thread executing our task.
This result highlights that the terminate() method indeed does not terminate worker threads that are running and instead only closes workers and helper threads that are not running.
Finally the main thread joins the ThreadPool and the running task completes normally, reporting its message.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
main reporting all threads: <_MainThread(MainThread, started 4460346880)> <DummyProcess(Thread-1, started daemon 123145354792960)> <DummyProcess(Thread-2, started daemon 123145371582464)> <DummyProcess(Thread-3, started daemon 123145388371968)> <DummyProcess(Thread-4, started daemon 123145405161472)> <DummyProcess(Thread-5, started daemon 123145421950976)> <DummyProcess(Thread-6, started daemon 123145438740480)> <DummyProcess(Thread-7, started daemon 123145455529984)> <DummyProcess(Thread-8, started daemon 123145472319488)> <Thread(Thread-9, started daemon 123145489108992)> <Thread(Thread-10, started daemon 123145505898496)> <Thread(Thread-11, started daemon 123145522688000)> Main is waiting a moment... Main is terminating the pool Main is waiting a moment... Main reporting all threads: <_MainThread(MainThread, started 4460346880)> <DummyProcess(Thread-1, started daemon 123145354792960)> Main joining the pool... This is coming from another thread Main done. |
Further Reading
This section provides additional resources that you may find helpful.
Books
- Python ThreadPool Jump-Start, Jason Brownlee (my book!)
- Threading API Interview Questions
- ThreadPool PDF Cheat Sheet
I also recommend specific chapters from the following books:
- Python Cookbook, David Beazley and Brian Jones, 2013.
- See: Chapter 12: Concurrency
- 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 ThreadPool: The Complete Guide
- Python Multiprocessing Pool: The Complete Guide
- Python ThreadPoolExecutor: The Complete Guide
- Python Threading: The Complete Guide
APIs
References
Takeaways
You now know that terminate() does not terminate workers in the ThreadPool in Python.
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Photo by Greg Wilson on Unsplash
Do you have any questions?