0

I was hoping to depend on atexit to stop a thread which would otherwise be blocking.

I found that the registered method will not be called when I call sys.exit() while the thread is running.

I found the function is called if the thread is not running.

    import os
    import atexit
    import threading
    import sys


    class Watcher:
        def __init__(self, to_watch, callback):
            self.path_to_watch = to_watch
            self.stop_flag = False
            self._monitor_thread = threading.Thread(target=self.monitor_thread)
            self._monitor_thread.start()

            atexit.register(self.stop)

        def stop(self):
            print(f'stopping watcher on {self.path_to_watch}')
            self.stop_flag=True


        def monitor_thread(self):
            while not self.stop_flag:
                pass

    if __name__ == '__main__':
        def my_callback( file, action):
            print(file, action)

        dw = Watcher('.', my_callback)
        sys.exit(0)

Is this expected behavior? I don't see any documentation of this in atexit.

Is there a better way to catch that the main thread is terminating and stop my threads?

Techniquab
  • 843
  • 7
  • 22
  • Insert a `self._monitor_thread.deamon = True` assignment statement right before the `self._monitor_thread.start()` in the `Watcher.__init__()` method. – martineau Mar 23 '20 at 18:28
  • This did the trick nicely, though what happens to the thread is a bit of a mystery to me, particularly if it was blocking in a winapi call. – Techniquab Mar 25 '20 at 21:33
  • Making it a daemon thread allows it to be abruptly terminated when the main program ends. Here's [a more detailed explanation](https://stackoverflow.com/questions/38804988/what-does-sys-exit-really-do-with-multiple-threads/38805873#38805873). – martineau Mar 25 '20 at 21:38
  • 1
    P.S. You might want to also replace the `pass` in the `monitor_thread()` function with `time.sleep(.001)` to give the main (any any other threads) a chance to run. – martineau Mar 25 '20 at 21:48

1 Answers1

-1

If you can structure the code so that there is (ideally) only one way for the main thread to exit, then a threading.Event is a good way for the main thread to signal one or more other threads that it is time to exit. Example:

import threading
import time
import sys

class Watcher:
    def __init__(self, done):
        self.done = done
        self._monitor_thread = threading.Thread(target=self.monitor_thread)
        self._monitor_thread.start()

    def monitor_thread(self):
        sys.stderr.write("Watcher started\n")
        while not self.done.is_set():
            pass
        sys.stderr.write("Watcher thread exiting\n")

if __name__ == '__main__':
    done = threading.Event()
    Watcher(done)
    time.sleep(5)
    done.set()
    sys.stderr.write("Main thread exiting\n")
  • It was lost when simplifying the code for the question, but the thread hangs in a blocking call creating the need to do more than setting an event, and I was trying to make the main thread agnostic of the monitor thread. – Techniquab Mar 24 '20 at 18:05
  • Thread cancellation can be a real mess. Check out https://stackoverflow.com/questions/323972/is-there-any-way-to-kill-a-thread for a description of how main can raise an exception in another thread. – Vincent Lucarelli Mar 24 '20 at 18:21
  • This still doesn't meet my needs since; "if the thread is busy outside the Python interpreter, it will not catch the interruption". Setting daemon to true let's me happily forget about the problem. Or, I can have my own callback mechanism at the end of the main thread which touches the directory being watched and everything cleans up nicely....When I learned about atexit it seemed like it should replace my "CleanExit" registration, but not quite. – Techniquab Mar 25 '20 at 21:30