Ending non-daemon threads when shutting down an in

2019-06-03 05:10发布

问题:

Please consider the code below:

#! /usr/bin/env python3

import threading
import time

class MyThread(threading.Thread):
    def __init__(self):
        super().__init__()
        self._quit_flag = False
    def run(self):
        while not self._quit_flag:
            print("Thread is alive!")
            time.sleep(0.500)
    def request_quit(self):
        self._quit_flag = True

mt = MyThread()
mt.start()

After saving this as 'test.py' and running 'python3 -i test.py', I get an interactive session, where the thread regularly prints the "Thread is alive!" message.

When I press control-D or run exit(), the python3 process does not terminate because the thread is still running. I want to fix that.

However, I do not want to make the thread a daemon thread -- I want to be able to end / join the thread properly, because in the real-life case I'm solving, I want to terminate a network connection nicely.

Is there a way to do this? For example, is some hook available that is executed after the REPL loop terminates, just prior to the exit of the main thread?

回答1:

Okay I found a way to do this myself.

Looking at the Python source-code, it turns out that at shutdown, Python attempts to join() all non-daemonic threads before completing the shutdown of the process. Which sort-of makes sense, except that this would be much more useful if there were a documented way to signal those threads. Which there isn't.

However, it is possible to re-implement the join() method of one's own class derived from Thread, which can that serve as a way to notify the subthread that it is supposed to shut itself down:

class MyThread(threading.Thread):
    def __init__(self):
        super().__init__()
        self._quit_flag = False
    def __del__(self):
        print("Bye bye!")
    def run(self):
        while not self._quit_flag:
            print("Thread is alive!",  threading.active_count(), threading.main_thread().is_alive())
            for t in threading.enumerate():
                print(t.is_alive())
            time.sleep(0.500)
    def request_quit(self):
        self._quit_flag = True

    def join(self):
        self.request_quit()
        super().join()

However, this way of addressing the issue is a pretty much a hack, and it ignores the following passage from the 'threading' module docs (https://docs.python.org/3/library/threading.html):

The Thread class represents an activity that is run in a separate thread of control. There are two ways to specify the activity: by passing a callable object to the constructor, or by overriding the run() method in a subclass. No other methods (except for the constructor) should be overridden in a subclass. In other words, only override the __init__() and run() methods of this class.

It does, however, work beautifully.



回答2:

I am not sure if this would be acceptable, but the only way I can see around your problem is to start the interactive console from your script instead of with -i flag. Then you would be able to carry on with your program to do the exit, after you terminate your interactive session:

import threading
import time
from code import InteractiveConsole

def exit_gracefully():
    print("exiting gracefully now")
    global mt
    mt.request_quit()
    mt.join()


class MyThread(threading.Thread):
    def __init__(self):
        super().__init__()
        self._quit_flag = False
    def run(self):
        while not self._quit_flag:
            print("Thread is alive!")
            time.sleep(0.500)
    def request_quit(self):
        self._quit_flag = True

mt = MyThread()
mt.start()
InteractiveConsole(locals=locals()).interact()
exit_gracefully()

This you would execute without -i flag, only as

python3 test.py

It would still give you the interactive console as with python -i, but now the script executes exit_gracefully() after the interactive shell exits.



回答3:

I think the way you can do this is to register a cleanup function using atexit and set your thread as a daemon, eg:

#! /usr/bin/env python3

import threading
import time
import atexit

class MyThread(threading.Thread):
    def __init__(self):
        super().__init__()

        # this ensures that atexit gets called.
        self.daemon = True
        self._quit_flag = False

    def run(self):
        while not self._quit_flag:
            print("Thread is alive!")
            time.sleep(0.500)
        print("cleanup can also go here")

    def request_quit(self):
        print("cleanup can go here")
        self._quit_flag = True


mt = MyThread()
def cleanup():
    mt.request_quit()
    mt.join()
    print("even more cleanup here, so many options!")

atexit.register(cleanup)
mt.start()