The ThreadPoolExecutor will block the Python interpreter from exiting if there are running tasks.
This can cause a program to hang before exiting, if exited normally, exiting with an exception, or exiting explicitly via a call to sys.exit().
In this tutorial, you will discover what happens if we forget or are unable to shut down the ThreadPoolExecutor.
Let’s get started.
Does the ThreadPoolExecutor Stop The Program From Exiting?
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:
1 2 3 4 5 6 7 |
... # create a thread pool with ThreadPoolExecutor() as tpe: # issue a task future = tpe.submit(task) # the the task result once the task is done result = future.result() |
You can learn more about the ThreadPoolExecutor in the tutorial:
One concern with the ThreadPoolExecutor is whether it will prevent the program from exiting if it is not explicitly shut down.
For example, we may not shut down the ThreadPoolExecutor correctly for many reasons, such as:
- We forget to call the shutdown() method.
- An error is raised that prevents a normal exit.
Sometimes in Python concurrency, a running thread or running child process can prevent the main process, and therefore the program from exiting normally.
Additionally, we may be concerned that if the ThreadPoolExecutor is not shut down properly, the worker threads will keep running if the main thread of the main process exits.
Will a running ThreadPoolExecutor prevent the program from exiting normally (or will the program hang)?
Run loops using all CPUs, download your FREE book to learn how.
ThreadPoolExecutor Workers Stop The Program From Exiting
A running ThreadPoolExecutor can stop the program from exiting, if there are running tasks.
This means that if we issue long-duration tasks to the ThreadPoolExecutor and attempt to exit the program normally or fail via an exception, then the program will not exit and instead will hang until all tasks in the thread pool have been completed.
This is by design.
The API documentation for the ThreadPoolExecutor indicates that the main thread will join the internal worker threads of the ThreadPoolExecutor when exiting, causing it to block until all threads in the ThreadPoolExecutor have terminated.
All threads enqueued to ThreadPoolExecutor will be joined before the interpreter can exit.
— concurrent.futures — Launching parallel tasks
If there are no running tasks in the ThreadPoolExecutor and the main thread attempts to exit, normally or with an exception, then the Python interpreter will shut down the ThreadPoolExecutor automatically for us and the program will exit.
You can learn more about closing the ThreadPoolExecutor implicitly via the Python interpreter in the tutorial:
This means that we should expect the program to continue running until tasks in the ThreadPoolExecutor have been completed normally.
It is possible to explicitly terminate the ThreadPoolExecutor, including all running tasks. For an example of this, see the tutorial:
Now that we know that the ThreadPoolExecutor will stop the program from exiting, let’s look at some worked examples.
Example of Running Tasks Stopping the Program From Exiting
In this section, we can explore a number of cases where a running task in the ThreadPoolExecutor prevents the Python interpreter from closing.
Running Task Prevents Normal Exit
We can explore the case where running tasks in the ThreadPoolExecutor stops a Python program from exiting.
In this example, we will define a task that takes a while to complete. We will then create a ThreadPoolExecutor, issue the task, then attempt to exit the program. The expectation is that the program will not exit until the task in the ThreadPoolExecutor has been completed.
Firstly, we can define a task that takes a moment to complete. A message is reported when the task is started, then the task blocks for two seconds to simulate computational effort then prints a final message to show that the task is done.
The task() function below implements this.
1 2 3 4 5 |
# task executed in the thread pool def task(): print('Task started') sleep(2) print('Task done') |
Next, we can create a ThreadPoolExecutor and issue the task.
1 2 3 4 5 |
... # create the thread pool tpe = ThreadPoolExecutor(10) # issue the task future = tpe.submit(task) |
The main thread will wait a brief moment, report a final message and then attempt to exit.
1 2 3 4 5 |
... # wait a moment sleep(0.5) # attempt to exit the program... print('Main done.') |
And that’s it.
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 |
# SuperFastPython.com # example of running task stops main from exiting from concurrent.futures import ThreadPoolExecutor from time import sleep # task executed in the thread pool def task(): print('Task started') sleep(2) print('Task done') # create the thread pool tpe = ThreadPoolExecutor(10) # issue the task future = tpe.submit(task) # wait a moment sleep(0.5) # attempt to exit the program... print('Main done.') |
Running the program first creates a ThreadPoolExecutor with 10 worker threads.
Next, the task is issued to the ThreadPoolExecutor and a Future object is returned.
The main thread then blocks for half a second.
The task begins running, reports a message, then sleeps.
The main thread continues running and reports a final message before attempting to exit.
The Python interpreter joins the threads in the ThreadPoolExecutor, blocking the program from exiting.
The task in the ThreadPoolExecutor resumes and reports a final message for exiting.
Finally, the threads in the ThreadPoolExecutor are terminated, and the program exits.
This highlights how a task running in the ThreadPoolExecutor prevents a normal exit from Python.
1 2 3 |
Task started Main done. Task done |
Running Task Prevents Exit on Underhanded Exception
We can explore the case where an unhandled exception in the main thread occurs, but the program is not able to exit because there are running tasks in the ThreadPoolExecutor.
In this example, we will update the above example to raise an Exception that is not handled. The expectation is that the exception causes the main thread to terminate but the program will not exit because of the running task in the ThreadPoolExecutor.
1 2 3 |
... # fail with a raised exception raise Exception('something bad happened') |
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 |
# SuperFastPython.com # example of running task stops main from exiting when an exception is raised from concurrent.futures import ThreadPoolExecutor from time import sleep # task executed in the thread pool def task(): print('Task started') sleep(2) print('Task done') # create the thread pool tpe = ThreadPoolExecutor(10) # issue the task future = tpe.submit(task) # wait a moment sleep(0.5) # fail with a raised exception raise Exception('something bad happened') |
Running the program first creates a ThreadPoolExecutor with 10 worker threads.
Next, the task is issued to the ThreadPoolExecutor and a Future object is returned.
The main thread then blocks for half a second.
The task begins running, reports a message, then sleeps.
The main thread continues running and raises an exception. The exception is not handled and terminates the main thread and details are reported.
The Python interpreter joins the threads in the ThreadPoolExecutor, blocking the program from exiting.
The task in the ThreadPoolExecutor resumes and reports a final message before exiting.
Finally, the threads in the ThreadPoolExecutor are terminated, and the program exits.
This highlights how a task running in the ThreadPoolExecutor prevents Python from exiting when an unhandled exception is raised.
1 2 3 4 5 6 |
Task started Traceback (most recent call last): ..., in <module> raise Exception('something bad happened') Exception: something bad happened Task done |
Running Task Prevents Exit via sys.exit()
We can explore the case where we attempt to explicitly exit the Python interpreter via a call to the sys.exit() function while tasks are still running in the ThreadPoolExecutor.
If you are new to terminating a program with sys.exit(), see the tutorial:
In this example, we update the above example to call the sys.exit() function from the main thread in an effort to immediately exit the program.
1 2 3 |
... # attempt to exit sys.exit(0) |
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 |
# SuperFastPython.com # example of running task stops main from exiting when calling sys.exit() from concurrent.futures import ThreadPoolExecutor from time import sleep import sys # task executed in the thread pool def task(): print('Task started') sleep(2) print('Task done') # create the thread pool tpe = ThreadPoolExecutor(10) # issue the task future = tpe.submit(task) # wait a moment sleep(0.5) # attempt to exit sys.exit(0) |
Running the program first creates a ThreadPoolExecutor with 10 worker threads.
Next, the task is issued to the ThreadPoolExecutor and a Future object is returned.
The main thread then blocks for half a second.
The task begins running, reports a message, then sleeps.
The main thread continues running and attempts to exit with a call to sys.exit(). The main thread is then terminated.
The Python interpreter joins the threads in the ThreadPoolExecutor, blocking the program from exiting.
The task in the ThreadPoolExecutor resumes and reports a final message before exiting.
Finally, the threads in the ThreadPoolExecutor are terminated, and the program exits.
This highlights how a task running in the ThreadPoolExecutor prevents Python from exiting when the main thread attempts to explicitly exit via a call to sys.exit().
1 2 |
Task started Task done |
Free Python ThreadPoolExecutor Course
Download your FREE ThreadPoolExecutor PDF cheat sheet and get BONUS access to my free 7-day crash course on the ThreadPoolExecutor API.
Discover how to use the ThreadPoolExecutor class including how to configure the number of workers and how to execute tasks asynchronously.
Example of No Running Tasks Not Stopping the Program From Exiting
We can explore the case where the Python interpreter will exit immediately if the ThreadPoolExecutor is not shut down and there are no running tasks.
In this example, we will start a ThreadPoolExecutor, issue a task that takes a moment, wait for the task to complete, then attempt to exit the program without shutting down the ThreadPoolExecutor. The expectation is that we can exit immediately because there are no tasks running.
We can update the above example to explicitly wait for the issued task to complete before attempting to exit the program.
1 2 3 |
... # wait for the task to complete _ = future.result() |
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 |
# SuperFastPython.com # example of no running tasks does not stop main from exiting from concurrent.futures import ThreadPoolExecutor from time import sleep # task executed in the thread pool def task(): print('Task started') sleep(2) print('Task done') # create the thread pool tpe = ThreadPoolExecutor(10) # issue the task future = tpe.submit(task) # wait for the task to complete _ = future.result() # attempt to exiting the program... print('Main done.') |
Running the program first creates a ThreadPoolExecutor with 10 worker threads.
Next, the task is issued to the ThreadPoolExecutor and a Future object is returned.
The main thread then blocks, waiting for the task to complete.
The task begins running, reports a message, then sleeps.
The task in the ThreadPoolExecutor resumes and reports a final message for exiting.
A final message is reported in the main thread and the program attempts to exit.
The Python interpreter joins the threads in the ThreadPoolExecutor. All threads in the ThreadPoolExecutor are terminated and the program exits immediately.
This highlights how the ThreadPoolExecutor does not stop the program from exiting when there are no running tasks.
1 2 3 |
Task started Task done Main done. |
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
Further Reading
This section provides additional resources that you may find helpful.
Books
- ThreadPoolExecutor Jump-Start, Jason Brownlee, (my book!)
- Concurrent Futures API Interview Questions
- ThreadPoolExecutor Class API Cheat Sheet
I also recommend specific chapters from the following books:
- 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 ThreadPoolExecutor: The Complete Guide
- Python ProcessPoolExecutor: The Complete Guide
- Python Threading: The Complete Guide
- Python ThreadPool: The Complete Guide
APIs
References
Takeaways
You now know what happens if we forget or are unable to shut down the ThreadPoolExecutor.
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Photo by Anton Savinov on Unsplash
Do you have any questions?