Last Updated on October 29, 2022
You can join a ThreadPool by calling join() on the pool after calling close() or terminate() in order to wait for all worker threads in the pool to terminate.
In this tutorial you will discover how to join a ThreadPool in Python.
Let’s get started.
Need to Wait for ThreadPool to Close
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 functions such as apply() or we can apply the same function to an iterable of items using functions 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 functions such as apply_async() and map_async().
The ThreadPool must be shutdown once we are finished with it in order to release the worker threads.
We often need to wait for the ThreadPool to close completely and release all resources before continuing on in our application.
How can we safely know when the ThreadPool is shut down completely?
Run loops using all CPUs, download your FREE book to learn how.
What is Joining?
Joining a resource is a common pattern in concurrent programming.
It is a mechanism on an active concurrency primitive that allows the caller to wait for the target primitive to finish.
It is implemented using a join() function on the target object.
This pattern is used with thread-based and process-based concurrency.
Joining Threads
For example, it is common for one thread to call join() another target thread to wait for the target thread to finish before continuing on in the application.
For example:
1 2 3 |
... # wait for the target thread to finish thread.join() |
You can learn more about joining threads in the tutorial:
It is also common for a thread to call join() on a thread-safe queue.Queue to wait for all tasks to be completed.
For example:
1 2 3 |
... # wait for all tasks in the target queue to be marked complete queue.join() |
You can learn more about joining thread-safe queues in the tutorial:
Joining Processes
It is also common for a parent process to call join() a target child process to wait for the target process to terminate before continuing on in the application.
1 2 3 |
... # wait for the target process to finish process.join() |
You can learn more about joining processes in the tutorial:
We can also call join() on a process-safe queue via the multiprocessing.JoinableQueue class.
This allows a process to wait until all tasks on the queue have been marked as done.
For example:
1 2 3 |
... # wait for all tasks to be marked as done queue.join() |
Next, let’s look at how we might also join a ThreadPool.
How to Join the ThreadPool Pool
We can join the ThreadPool by calling the join() method.
For example:
1 2 3 |
... # join the thread pool pool.join() |
Calling join() on the ThreadPool will allow the caller to wait for all worker threads in the ThreadPool to be closed completely.
If all worker threads in the pool have already terminated, then the call to join() will return immediately, otherwise it will block and return only once all threads have terminated.
Wait for the worker processes to exit. One must call close() or terminate() before using join().
— multiprocessing — Process-based parallelism
Although the API documentation describes worker processes, in the ThreadPool, we will be waiting for worker threads.
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
Why Join a ThreadPool?
We may want to wait for the ThreadPool to be completely shut down for many reasons, such as:
- Wait for all issued tasks to finish, before exiting.
- Wait for the resources to be released before continuing on with an application.
- Wait for the pool to be completely closed before closing and releasing other resources during a program exit.
For example, if we issue tasks from the main thread then exit the main thread without joining the ThreadPool, then the ThreadPool will be forcefully closed and the tasks in the pool will not complete.
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
When to Join a ThreadPool?
We can only join a ThreadPool after it has been shut down.
Recall that a ThreadPool can be shutdown by explicitly calling the close() method or the terminate() method.
For example:
1 2 3 |
... # close the thread pool pool.close() |
You can learn more about shutting the thread pool down in the tutorial:
Only once the pool has been closed can we join the ThreadPool.
Therefore the idiom for joining a ThreadPool is to first shutdown the pool and then join.
For example:
1 2 3 4 5 |
... # close the thread pool pool.close() # join the thread pool pool.join() |
If we do not close the pool first before joining the ThreadPool, then an error will be raised.
Now that we know how to join the ThreadPool, let’s look at some worked examples.
Example of Joining a ThreadPool After Close
We can explore how to join a ThreadPool.
In this example we will create a ThreadPool, issue a task, then close the ThreadPool and wait for all threads to terminate.
Firstly, we can define a new custom function to execute as a task in the pool which will report a message and sleep for a moment.
The task() function below implements this.
1 2 3 4 5 6 7 8 |
# task executed in a worker thread def task(): # report a message print(f'Task executing') # block for a moment sleep(1) # report a message print(f'Task done') |
Next, in the main thread we can create a ThreadPool with a default configuration.
1 2 3 |
... # create and configure the thread pool pool = ThreadPool() |
Next, we can issue our task() function to the ThreadPool asynchronously.
Notice that we don’t have to wait for the task to complete.
1 2 3 |
... # issue tasks to the thread pool result = pool.apply_async(task) |
Next, we can close the ThreadPool.
This will prevent any further tasks from being issued to the ThreadPool, but will allow currently executing tasks in the pool to complete.
1 2 3 |
... # close the thread pool pool.close() |
We can then call join() to wait for all tasks to complete and all worker threads to close.
1 2 3 |
... # wait a moment pool.join() |
Finally, we can report a message that the application is exiting.
1 2 3 |
... # report a message print(f'Main done') |
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 |
# SuperFastPython.com # example of joining a thread pool after calling close from time import sleep from multiprocessing.pool import ThreadPool # task executed in a worker thread def task(): # report a message print(f'Task executing') # block for a moment sleep(1) # report a message print(f'Task done') # protect the entry point if __name__ == '__main__': # create and configure the thread pool pool = ThreadPool() # issue a task to the thread pool pool.apply_async(task) # close the thread pool pool.close() # wait a moment pool.join() # report a message print(f'Main done') |
Running the example first creates the ThreadPool then issues the task to the ThreadPool.
The ThreadPool begins executing the task in a worker thread.
The main thread then closes the ThreadPool while the task is running.
This prevents the pool from taking any further tasks, then closes all worker threads once all tasks are completed.
The main thread then joins the ThreadPool, blocking until all worker threads are closed and released.
The task in the ThreadPool finishes and the worker thread in the pool is closed.
The main thread carries on and reports a final message.
1 2 3 |
Task executing Task done Main done |
Next, let’s take a closer look at joining the ThreadPool after terminating it.
Example of Joining a ThreadPool After Terminate
We can explore how to join the ThreadPool after calling terminate().
Recall that the terminate() function will forcefully close all threads in the ThreadPool immediately.
Nevertheless, it may still take a moment for the pool to shut down immediately. We can call join() to be sure that all worker threads have finished after calling terminate(), before continuing on.
1 2 3 4 5 |
... # terminate the thread pool pool.terminate() # wait a moment pool.join() |
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 |
# SuperFastPython.com # example of joining a thread pool after calling terminate from time import sleep from multiprocessing.pool import ThreadPool # task executed in a worker threads def task(): # report a message print(f'Task executing') # block for a moment sleep(1) # report a message print(f'Task done') # protect the entry point if __name__ == '__main__': # create and configure the thread pool pool = ThreadPool() # issue a task to the thread pool pool.apply_async(task) # terminate the thread pool pool.terminate() # wait a moment pool.join() # report a message print(f'Main done') |
Running the example first creates the ThreadPool then issues the task to the ThreadPool.
The ThreadPool begins executing the task in a worker thread. In this case, it does not get a chance to print the first message.
The main thread then terminates the ThreadPool while the task is running.
This prevents the pool from taking any further tasks, then closes all worker threads (almost) immediately.
The main threads then joins the ThreadPool, blocking until all worker threads are closed and released.
The task in the ThreadPool does not get a chance to start completely, let alone finish, and the worker threads in the pool are terminated.
The main thread carries on and reports a final message.
1 |
Main done |
Next, let’s take a look at what happens if we attempt to join a ThreadPool before shutting it down.
Error When Joining a ThreadPool Without Shutting Down
We can explore the case where we attempt to join the ThreadPool without shutting it down first.
1 2 3 |
... # wait a moment pool.join() |
In this case, we expect an error to be raised indicating that this is not valid.
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 |
# SuperFastPython.com # example of joining a thread pool without shutting it down from time import sleep from multiprocessing.pool import ThreadPool # task executed in a worker threads def task(): # report a message print(f'Task executing') # block for a moment sleep(1) # report a message print(f'Task done', flush=True) # protect the entry point if __name__ == '__main__': # create and configure the thread pool pool = ThreadPool() # issue a task to the thread pool pool.apply_async(task) # wait a moment pool.join() # report a message print(f'Main done') |
Running the example first creates the ThreadPool then issues the task to the ThreadPool.
The ThreadPool begins executing the task in a worker thread.
The main threads then attempts to join the ThreadPool.
An exception is raised immediately, as expected, indicating “Pool is still running“.
1 2 3 4 5 6 |
Traceback (most recent call last): ... Task executing pool.join() ... ValueError: Pool is still running |
What if We Don’t Join the ThreadPool
We can explore the case of what happens if we do not join the ThreadPool after shutting it down.
For example, we may call close() to shutdown the ThreadPool and allow the issued tasks to complete first.
1 2 3 |
... # close the thread pool pool.close() |
We then do not join the pool and instead report a message and allow the main thread to exit.
1 2 3 |
... # report a message print(f'Main done') |
In this case, we expect the Python garbage collector to terminate the ThreadPool and immediately stop all worker threads, not allowing the issued task to complete.
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 |
# SuperFastPython.com # example of not joining a thread pool after calling close from time import sleep from multiprocessing.pool import ThreadPool # task executed in a worker threads def task(): # report a message print(f'Task executing') # block for a moment sleep(1) # report a message print(f'Task done') # protect the entry point if __name__ == '__main__': # create and configure the thread pool pool = ThreadPool() # issue a task to the thread pool pool.apply_async(task) # close the thread pool pool.close() # report a message print(f'Main done') |
Running the example first creates the ThreadPool then issues the task to the ThreadPool.
The ThreadPool begins executing the task in a worker thread.
The main thread then does not join the ThreadPool, instead, it reports a message and exits.
The Python garbage collector finalizes the ThreadPool which ultimately results in the terminate() function being called automatically on the pool.
This terminates all threads immediately, preventing the issued task from finishing.
This example highlights an important case of why we might need to join the ThreadPool, specifically to allow issued tasks to finish before exiting the application.
1 |
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 how to join a ThreadPool in Python.
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Do you have any questions?