Given a Python program that creates and spins off a thread
[. . . some code. . .]
my_thread = Thread(target=some_process)
[. . . more code. . .]
. . . and everything is happening just wonderfully. The program is humming along and the thread is happily threading along too. . .
Until. . . something isn’t wonderful and an exception is thrown somewhere else in the program, (not the thread), with all the traceback fanfare about an issue that has nothing to do with the thread.
What happens to the thread? Does the rob-rob-robin keep bob-bob-bobbin’ along? Or does the thread automatically get the axe when the main process spilled its guts?
If the main() is well designed, (try, except, finally with a stop thread flag var, the except will handle the exception and the finally block will tell the thread to stop, and wait at a join() for the thread to complete.
If an exception in main is not handled, causing main to exit, the thread will just keep happily doing its thing until it completes or has an unhandled exception. (Or is killed by the user - ps -ef | grep progname followed.by kill -9 nnnn)
I have some examples to show exceptions in a thread, with and without a join in main, as well as exceptions in main, with and without catch and finally join - if you want to delve into the three good design, and three bad design cases.
I read about threading and several sites, (that I don’t remember right now), mentioned that the thread should be “killed with a join()”.
I had my doubts because both this code and other examples included specific “kill [with a] 9mm [hollow-point]” functionality. This is why I asked this specific question.
Another question:
Assume the programs as noted above where the main routine spins off a thread that wanders off and does whatever it wants.
Also assume that something happens to the thread - it abends, it gets stuck, it whatever’s in such a way that it never returns.
The main routine sends a polite “kill” (sigterm) request and waits at the “join()” pub to have a beer.
In this case, the thread never returns or responds. What happens? Does the main routine spend all night at the pub waiting for a thread that will never show up? Do I have to include a “Humm. . . it’s getting a bit late.” routine to the original command to shut down the thread that will send it a “kill (9mm Glock)” message if it never returns to make sure it isn’t hanging out with a bad crowd?
So in the case of a thread that will not end (or finish)
The user will have to open another terminal and kill both the thread thread and possibly the main thread. The main may end naturally when the user kills the thread, but there are sometimes zombie threads that only clear from the process list via a reboot
The Kingston Trio recorded this in response to the “T”, (Boston’s mass transit system), wanting to impose an “exit fare” at certain stations. In this song, the character, (Charlie), didn’t have the extra money required to leave the station so he was “stuck” on the subway and became “The Man Who Never Returned”.
Back in the early 2000’s, the Boston Transit Authority - (the “T”), instituted a reusable “proximity” type card that could store value and be used to pay the turnstile fare - and it became known as the “Charlie Card”.
(I think I have one kicking around somewhere. . .)
P.S.
They had to change their name from the “MTA” as sung in the song, because that was already taken by the New York Subway’s transit authority.
Absolutely my favorite group of childhood - learned to play guitar chords with their songs. My “mix tape” (record stack) was Harry Belafonte, Kingston Trio, Mothers Of Invention, Peter Paul And Mary.
Allow re-syncronization with main, allow “returning something” to main. The join() function of a thread actually runs in the caller’s thread - the main() thread.
This is an example of:
printing a thread exception and traceback in the thread
then passing the exception back to the main via the join()
$ ./handle_thread_exception_in_main.py
09:26:47: Thread-1 Causing an exception in thread
09:26:47: Thread-1: Printing traceback in the thread exception handler
File "/usr/lib/python3.7/threading.py", line 885, in _bootstrap
self._bootstrap_inner()
File "/usr/lib/python3.7/threading.py", line 917, in _bootstrap_inner
self.run()
File "./handle_thread_exception_in_main.py", line 30, in run
traceback.print_stack()
09:26:47: MainThread: re-raising exception in thread.join()
09:26:47: Exception Handled in Main, Details of the Exception: division by zero
The program:
#!/usr/bin/env python3
# Handle a thread exception in the main
# Importing the modules
import threading
import traceback
import logging
# Custom Thread Class
class MyThread(threading.Thread):
# Function that raises the exception
def someFunction(self):
name = threading.current_thread().name
logging.info("%s Causing an exception in thread ",name)
divbyzero=1/0
def run(self):
# Variable that stores the exception, if raised by someFunction
self.exc = None
try:
self.someFunction()
except Exception as e:
name = threading.current_thread().name
logging.info("%s: Printing traceback in the thread exception handler")
traceback.print_stack()
# now save the exception for later re-raising to the main
self.exc = e
def join(self):
threading.Thread.join(self)
# Since join() returns in caller thread
# we re-raise the caught exception
# if any was caught
if self.exc:
name = threading.current_thread().name
logging.info("%s: re-raising exception in thread.join()", name)
raise self.exc
# MAIN function
def main():
# set up logger
format = "%(asctime)s: %(message)s"
logging.basicConfig(format=format, level=logging.INFO,
datefmt="%H:%M:%S")
# Create a new Thread t
# Here Main is the caller thread
t = MyThread()
t.start()
# Exception handled in main thread
try:
t.join()
except Exception as e:
logging.info("Exception Handled in Main, Details of the Exception: %s", e)
if __name__ == '__main__':
main()
The trouble is that good design means putting error handlers all over the place to gracefully clean up any unexpected mess that might occur as well as expected exceptions like control-c.
Threads introduce complexity, and Python’s Global Interpreter Lock (GIL) complicates the thinking with threads even more by making only one thread execute at a time.
Threading: Best for allowing program to remain active while also waiting for I/O
MultiProcessing: Best for spreading computationally intensive tasks across cores of processor for true simultaneous execution