ThreadPoolExecutor Workers Stop Main Thread From Exiting
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:
...
# 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)?
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.
# 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.
...
# 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.
...
# 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.
# 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.
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.
...
# fail with a raised exception
raise Exception('something bad happened')
Tying this together, the complete example is listed below.
# 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.
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.
...
# attempt to exit
sys.exit(0)
Tying this together, the complete example is listed below.
# 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().
Task started
Task done
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.
...
# wait for the task to complete
_ = future.result()
Tying this together, the complete example is listed below.
# 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.
Task started
Task done
Main done.
Takeaways
You now know what happens if we forget or are unable to shut down 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.