5

When I run a python program (2.7 or 3), I import a module that has a class that initializes some threads. The problem is whenever an uncaught exception occurs in the main thread, the main function dies yet the thread keeps running like a zombie causing the python process to never die.

What's the best way to have any uncaught exception in the main thread (or even other threads) to kill everything everywhere.

Since I often call the subprocess module, I usually use threading.Event to help exit cleanly. However, uncaught exceptions won't trigger those events.

Here's an example of a program where the thread just won't die....

prog1.py

#!/usr/bin/env python2.7

import threading
import modx1

mod_obj = modx1.Moddy()

raise Exception('DIE UNEXPECTEDLY')

try:
    raise Exception('known problem here')
except Exception:
    mod_obj.kill_event.set()

modx1.py

#!/usr/bin/env python2.7

import threading
import subprocess
from time import sleep


class Moddy():
    def __init__(self):
        self.kill_event = threading.Event()
        self.my_thread=threading.Thread(target=self.thread_func)
        self.my_thread.start()
    def thread_func(self):
        while  not self.kill_event.is_set():
            print('thread still going....')
            sleep(2)
MookiBar
  • 183
  • 2
  • 14
  • see https://stackoverflow.com/questions/2564137/how-to-terminate-a-thread-when-main-program-ends Maybe the daemon flag is what you want? – JL Peyret Jan 22 '21 at 19:30
  • I looked at that. I don't need a generic idea of daemon threads and/or using events. My specific concerns are not addressed directly in that post. Although the code itself doesn't show it, I can't use daemon threads because of all the spawned processes in a lot of my code.
    For the answer that I accepted here, I did see the solution ("threading.main_thread().is_alive()") in an unaccepted answer at the bottom of that post. But, again, this post is not a duplicate of that one.
    – MookiBar Jan 22 '21 at 21:16

2 Answers2

2

This seems to be hackish, but still something to start with.

prog1.py - I have added sleep of 5 seconds so you can see the thread running and then gets completed.

import modx1
import time


mod_obj = modx1.Moddy()
time.sleep(5)
raise Exception('DIE UNEXPECTEDLY')

try:
    raise Exception('known problem here')
except Exception:
    mod_obj.kill_event.set()

modx1.py -> Check for the boolean you set and also the status of main thread.

import ctypes
import threading
import subprocess
from time import sleep


class Moddy():
    def __init__(self):
        self.kill_event = threading.Event()
        self.my_thread = threading.Thread(target=self.thread_func)
        self.my_thread.start()

    def thread_func(self):
        while not self.kill_event.is_set() and threading.main_thread().is_alive():
            print('thread still going....')
            sleep(2)

OUTPUT

thread still going....
thread still going....
thread still going....
Traceback (most recent call last):
  File "/Users/gaurishankarbadola/PycharmProjects/untitled1/prog1.py", line 8, in <module>
    raise Exception('DIE UNEXPECTEDLY')
Exception: DIE UNEXPECTEDLY

Process finished with exit code 1
gsb22
  • 2,112
  • 2
  • 10
  • 25
  • threading.main_thread.is_alive() !! I didn't know that was a thing! Thank you! This is what I needed! – MookiBar Jan 22 '21 at 20:54
  • Considering that i use threading Events to shut everything down cleanly, I'm starting to think ```threading.main_thread().is_alive()``` could easily stand in to replace any such events... – MookiBar Jan 22 '21 at 21:19
  • @MookiBar yes, that could be a possibility, but sometimes your main thread might be running and you still want to kill the child thread and in that scenario, your `self.kill_event.is_set()` logic is correct. – gsb22 Jan 22 '21 at 21:39
2

Assuming you're not reliant on clean up occurring in the threads, the simplest solution is to just launch all your threads as daemon threads; a Python program exits when all non-daemon threads have completed, so if your main thread is the only non-daemon thread, the program will terminate when the main thread does.

The change required is trival, just change:

self.my_thread=threading.Thread(target=self.thread_func)

to:

self.my_thread=threading.Thread(target=self.thread_func, daemon=True)

and the thread will be launched as a daemon.

If you do have cleanup that must occur and isn't automatic on program termination (e.g. sending some "I'm done" message to a remote machine over the network, where simply closing the connection isn't sufficient for whatever reason), daemon won't work (the threads are forcibly terminated, whatever they were in the middle of), so you can use your approach, you just need to expand the try block to cover all code from the moment the thread is launched so the handling always occurs. I'd also recommend set-ing the Event in a finally block, not an except block, if you want to always tell the threads to clean up, not just when something bad happens. If it should be done only on an exception, I'd use a bare except: block, and explicitly re-raise after setting Event, e.g.:

try:
    # Entire program after thread launches
except:
    mod_obj.kill_event.set()
    raise  # Reraises exception as if it were never caught, rather than silencing it
ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
  • hey, just as my comment was written. good to know it's applicable, my threading-fu is sadly lacking. – JL Peyret Jan 22 '21 at 19:31
  • Unfortunately, daemon threads aren't ideal. Like i said, i use 'subprocess' a lot, and daemon threads would kill them in an unclean way (zombie processes). – MookiBar Jan 22 '21 at 20:49
  • Also unfortunate that the latter half means I'd have to put the entire code into a try/except block. Is there not something similar to a flag or signal that can do the trick? – MookiBar Jan 22 '21 at 20:51
  • @MookiBar: You could put a handler in [`sys.excepthook`](https://docs.python.org/3/library/sys.html#sys.excepthook) to do the same job. The flaw is that it's a single, global handler, and some third-party libraries might rely on it as well, causing conflicts if all overriders aren't careful to preserve the hook they replaced and call it when they're done. It's not *that* hard to put all the code in the `try` block though. If you hate extra indentation, put the code that would be in the `try` block in another function, and make the contents of the `try` block a call to that function. – ShadowRanger Jan 23 '21 at 01:00
  • Good point. I tend to put everything after the obligatory ```if __name__ == "__main__"``` into a seperate function, so it wouldn't be the worst thing in the world. I've just had others look down on me for ```try/except```ing my whole code... – MookiBar Jan 23 '21 at 01:45