Last Updated on October 29, 2022
You can automatically close threads once you are finished with them using the context manager interface.
In this tutorial you will discover how to use the context manager interface of the ThreadPool class in Python.
Let’s get started.
Need to Automatically Shutdown 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 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 closed once we are finished with it in order to release the worker threads.
How can we safely and automatically close the ThreadPool once we are finished with it?
Run loops using all CPUs, download your FREE book to learn how.
What is a Context Manager
A context manager is an interface on Python objects for defining a new run context.
Python’s with statement supports the concept of a runtime context defined by a context manager. This is implemented using a pair of methods that allow user-defined classes to define a runtime context that is entered before the statement body is executed and exited when the statement ends
— CONTEXT MANAGER TYPES, BUILT-IN TYPES
A run context is a block of Python code. Examples of other run contexts include the content of a function or the content of a loop.
Context managers allow an object to define the code that runs at the beginning and end of a block of code. This is a lot like the try-except-finally pattern, except the code executed before and after the block is hidden within the object and only the body block of code needs to be specified.
The context manager interface has two methods that must be implemented on an object that supports the interface, they are:
- __enter__(): Executed prior to the code block.
- __exit__(): Executed after the code block.
These two methods are always executed, even if an error or exception occurs within the block. In this way, __exit__() acts like a finally block in a try-except-finally pattern.
The with statement is used to wrap the execution of a block with methods defined by a context manager . This allows common try…except…finally usage patterns to be encapsulated for convenient reuse.
— THE WITH STATEMENT
Context managers on Python objects are used via the “with” statement.
The with statement takes the object instance that implements the context manager interface as an argument. It will execute the code in the object for the beginning of the block, e.g. the __enter__() method and return an instance of an object that can be assigned.
Context managers are useful for operations that require the consistent initialization and shutdown of an operation.
The context manager handles the entry into, and the exit from, the desired runtime context for the execution of the block of code.
— WITH STATEMENT CONTEXT MANAGERS
Common examples of context managers provided in the Python standard library include:
- Opening and closing files.
- Opening and closing sockets.
Now that we know what context managers are, let’s look at the use of context managers with the ThreadPool.
How to Close a ThreadPool
The ThreadPool provides a pool of worker threads.
As such, the pool must be closed directly once we are finished with it to allow the threads and their resources to be released. Otherwise, the worker threads and their resources will remain until the program exits.
multiprocessing.pool objects have internal resources that need to be properly managed (like any other resource) by using the pool as a context manager or by calling close() and terminate() manually. Failure to do this can lead to the process hanging on finalization.
— multiprocessing — Process-based parallelism
In fact, not closing the ThreadPool and relying on the Python garbage collector to close it once we are finished with it can result in the program hanging.
Note that it is not correct to rely on the garbage collector to destroy the pool as CPython does not assure that the finalizer of the pool will be called
— multiprocessing — Process-based parallelism
These comments in the API documentation refer to the Pool class that uses process-based concurrency. Recall that the ThreadPool class extends the Pool class. Perhaps some of these concerns are relevant to the ThreadPool class as well.
There are two ways to safely close the ThreadPool, they are:
- close() method: close the pool once all issued tasks are complete, will not accept additional tasks.
- terminate() method: close the worker threads immediately, will not accept additional tasks.
As such, the normal usage pattern for the ThreadPool is to:
- Create and configure the ThreadPool.
- Issue tasks to the pool.
- Close the pool.
We need to ensure that the pool is closed, even if an error is raised while tasks are issued to the pool and their results are handled.
There are three main ways we could handle this, they are:
- Directly.
- With a Try-Except-Finally pattern.
- With a Context Manager.
Let’s take a closer look at each in turn.
Approach 1: Directly
We can create, use and close the pool directly.
This can be achieved by calling the class constructor to create and configure the pool.
Issuing tasks to the pool using methods such as apply() and map().
Then, calling the close() method on the pool and perhaps join() to ensure all remaining tasks are completed.
For example:
1 2 3 4 5 6 7 8 9 |
... # create the thread pool pool = ThreadPool() # issue tasks to the pool ... # close the thread pool pool.close() # wait for issued tasks to complete pool.join() |
If we don’t call the join() function and there are tasks executing the pool, it is possible that the pool will be forcefully terminated by the Python garbage collector and the tasks will not complete.
The danger of this approach is that an Error or Exception could be raised while issuing tasks to the pool and handling their results.
For example:
1 2 3 4 5 6 7 8 9 10 11 |
... # create the thread pool pool = ThreadPool() # issue tasks to the pool ... raise Exception('Something bad happened') # ... # close the thread pool pool.close() # wait for issued tasks to complete pool.join() |
This is a problem because the pool will not be closed in a controlled manner and it is possible that it will be forcefully terminated, stopping any tasks that are being executed.
Approach 2: Try-Except-Finally
A better approach is to use a try-except-finally pattern.
The issuing of tasks and handling of results can be performed within the try block. Any expected errors or exceptions can be handled in the except block, if desired. The pool can then be closed safely in the finally block.
For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
... # create the thread pool pool = ThreadPool() try: # issue tasks to the pool # ... except: # handle expected errors and exceptions finally: # close the thread pool pool.close() # wait for issued tasks to complete pool.join() |
This is an improvement over directly closing the pool because it ensures that the safe shutdown procedure for the pool is always executed, even if an exception is raised.
Using a try-finally or a try-except-finally pattern means that if an unexpected exception is raised, any other tasks issued to the pool will have a chance to complete before the pool is gracefully closed.
In other programming languages like Java and C#, the try-except-finally pattern is a best practice when creating, using and releasing a resource such as a ThreadPool.
It is not a best practice in Python, because we have something better.
Approach 3: Context Manager
Python provides a context manager interface on the ThreadPool.
This achieves a similar outcome to using a try-except-finally pattern, with less code.
Specifically, it is more like a try-finally pattern, where any exception handling must be added and occur within the code block itself.
For example:
1 2 3 4 5 6 |
... # create and configure the thread pool with ThreadPool() as pool: # issue tasks to the pool # ... # close the pool automatically |
There is an important difference with the try-finally block.
If we look at the source code for the multiprocessing.pool.Pool class, we can see that the __exit__() method calls the terminate() method on the pool when exiting the context manager block.
This means that the pool is forcefully closed once the context manager block is exited. It ensures that the resources of the ThreadPool are released before continuing on, but does not ensure that tasks that have already been issued are completed first.
Well, this would be true of threads support direct termination.
Unfortunately, they do not, therefore a call to terminate() operates just like a call to close(), and does not immediately stop workers that are running. This is not documented in the API docs.
The worker threads are daemon threads, so if the main thread exits, it will terminate workers, even if they are running.
If we wish to wait for all issued tasks to complete before closing the pool, we must explicitly add a call to close() and join().
For example:
1 2 3 4 5 6 7 8 9 10 |
... # create and configure the thread pool with ThreadPool() as pool: # issue tasks to the pool # ... # close the thread pool pool.close() # wait for issued tasks to complete pool.join() # close the pool automatically |
It means that if we fail to close the pool safely due to an unexpected error or exception, the pool will always be terminated, allowing worker threads to be released and ensuring our main thread does not hang waiting for the pool.
Now that we know the benefit of context managers when using thread pools, let’s look at some examples.
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
Example of the ThreadPool with a Context Manager
We can explore how to use the context manager interface of the ThreadPool.
In this example, we will define a custom function to execute in the ThreadPool. We will then create and configure a ThreadPool using the context manager interface. The task will be issued, then we will safely close the ThreadPool.
Firstly, let’s define a task to execute in the ThreadPool.
The task will report a message to indicate that it is starting, blocks for one second, then reports a message that it has finished.
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', flush=True) # block for a moment sleep(1) # report a message print(f'Task done', flush=True) |
Next, in the main thread will create and configure the ThreadPool using the context manager interface.
1 2 3 4 |
... # create and configure the thread pool with ThreadPool() as pool: # ... |
Next, we will issue a task to the ThreadPool to execute our custom function asynchronously and not wait for the result.
1 2 3 |
... # issue tasks to the thread pool _ = pool.apply_async(task) |
We can then close the ThreadPool and wait for the issued tasks to complete.
1 2 3 4 5 |
... # close the thread pool pool.close() # wait for all tasks to complete 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 |
# SuperFastPython.com # example of using the thread pool context manager 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 with ThreadPool() as pool: # issue tasks to the thread pool _ = pool.apply_async(task) # close the thread pool pool.close() # wait for all tasks to complete pool.join() |
Running the example first creates the ThreadPool.
A task is issued to the ThreadPool, then the main thread closes the pool and waits for issued tasks to complete.
The ThreadPool executes the task, reports the messages, then finishes.
The main thread continues on. The ThreadPool is then terminated automatically by the context manager interface, although it has no effect in this case.
1 2 |
Task executing Task done |
An alternate pattern when using the context manager interface is to explicitly block while waiting for the task result and allow the thread pool to close automatically via the context manager interface.
This can be achieved by getting the AsyncResult returned from apply_async() and waiting on the result via a call to wait().
For example:
1 2 3 4 5 |
... # issue tasks to the thread pool result = pool.apply_async(task) # wait for the result result.wait() |
In this way, we do not need to explicitly close the ThreadPool and wait for all issued tasks to complete.
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 |
# SuperFastPython.com # example of using the thread pool context manager and waiting for the result 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 with ThreadPool() as pool: # issue tasks to the thread pool result = pool.apply_async(task) # wait for the result result.wait() |
Running the example first creates the ThreadPool.
A task is issued to the thread pool and an AsyncResult is returned. The main thread blocks on the result, waiting for the task to finish.
The ThreadPool executes the task, reports the messages, then finishes.
The main thread continues on. The ThreadPool is then terminated automatically by the context manager interface, releasing all of the resources.
1 2 |
Task executing Task done |
Next, let’s consider what happens if the context manager closes the ThreadPool while tasks are running.
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
Example of Exiting the Context Manager With Running Tasks
The context manager interface of the ThreadPool will close the worker threads in the pool.
The terminate() method on the ThreadPool class is called, but running worker threads will not be terminated. This is because threads cannot be terminated immediately in the same way as processes. Therefore, there may be tasks running after exiting the context manager block.
Nevertheless, because the worker threads are daemon threads, the main thread can exit which will terminate the program and any worker threads that are still running.
We can update the example from the previous section to demonstrate this.
The task can be issued to the ThreadPool, then the main thread can block for a fraction of a second then exit the context manager block of the ThreadPool. This will terminate the task half-way through.
For example:
1 2 3 4 5 6 7 |
... # create and configure the thread pool with ThreadPool() as pool: # issue tasks to the thread pool _ = pool.apply_async(task) # block for a moment sleep(0.5) |
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 |
# SuperFastPython.com # example of using the thread pool context manager and forcefully terminating running tasks 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 with ThreadPool() as pool: # issue tasks to the thread pool _ = pool.apply_async(task) # block for a moment sleep(0.5) |
Running the example first creates the ThreadPool.
A task is issued to the ThreadPool. The main thread blocks for a fraction of a second.
The ThreadPool starts executing the task and reports the first message.
The main thread continues on and exits the context manager. This results in the terminate() method on the ThreadPool being executed automatically.
The worker threads in the pool are closed. Idle threads are closed immediately and any running threads and left running.
The main program then exits which terminates any daemon worker threads that are still running.
1 |
Task executing |
This highlights that care must be taken to wait for running tasks when using the context manager interface of the ThreadPool.
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 use the context manager interface of 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 Daniel Klein on Unsplash
Do you have any questions?