关闭交互式Python会话时如何结束非守护线程

4
请看下面的代码:

请考虑以下代码:

#! /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()

将此内容保存为“test.py”,并运行“python3 -i test.py”后,会得到一个交互式会话,其中线程定期打印“Thread is alive!”消息。
当我按Control-D或运行exit()时,python3进程不会终止,因为线程仍在运行。 我想解决这个问题。
但是,我不想将线程设置为守护线程——我想能够正确地结束/加入线程,因为在我解决的实际情况下,我想要优雅地终止网络连接。
是否有一种方法可以做到这一点? 例如,是否有可用的钩子,在REPL循环终止之后,主线程退出之前执行?

你是否了解 atexit - Bi Rico
4个回答

3

好的,我找到了一种自己解决这个问题的方法。

查看Python源代码后发现,在关闭过程中,Python会尝试加入(join)所有非守护线程,然后才完成进程的关闭。这有点合理,但如果有一种记录方式来信号这些线程就更有用了。可惜的是,这样的方式并不存在。

然而,我们可以重新实现继承于Thread的类的join()方法,这可以作为一种通知子线程应该关闭的方法:

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()

然而,这种解决问题的方式相当于一种Hack,它忽略了“threading”模块文档中以下的内容(https://docs.python.org/3/library/threading.html):

Thread类代表在单独的控制线程中运行的活动。有两种指定活动的方法:通过将可调用对象传递给构造函数,或者通过在子类中覆盖run()方法。除了构造函数之外,不应该在子类中重写任何其他方法。换句话说,只能覆盖此类的__init__()和run()方法。

然而,这种方式确实非常有效。


这实际上非常有用。尽管应该避免使用,但如果程序即将退出,即使它破坏了一些东西也不会有太大影响。 - Hannu
我们能否将此提交给Python开发人员考虑?允许覆盖join似乎是一个有用的功能,除非它会破坏我们没有看到的某些东西(比如在Windows上?) - Goswin von Brederlow
1
很不幸,自从Python 3.7.4版本以后,这个方法已经无法使用了。"threading"模块的_shutdown方法已经被更改,不再调用join()方法。唉。 - reddish
1
我已经在介绍这个更改的 PR 上进行了评论:https://github.com/python/cpython/pull/13948 - Will Manley

1

我不确定这是否可行,但我能想到的解决你的问题的唯一方法是从你的脚本中启动交互式控制台,而不是使用-i标志。然后,在终止交互会话后,您将能够继续执行程序以进行退出:

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()

这个命令应该不带 -i 标记执行,只需要这样:

python3 test.py

这样做仍然会给您提供交互式控制台,就像使用python -i一样,但现在脚本在交互式shell退出后执行exit_gracefully()


我感谢您提出的解决方法,但不幸的是,在我的实际情况中,这种方法行不通;我需要一种不涉及启动专用解释器的解决方案。 - reddish
我担心那里会有某种限制,但还是想提供解决方案。很高兴你能解决这个问题。 - Hannu

1

我认为你可以通过使用atexit注册一个清理函数,并将你的线程设置为守护线程来实现这一点,例如:

#! /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()

不,那样行不通。atexit例程在Python关闭过程尝试加入所有未终止的非守护线程后调用,如果它们没有终止,则会阻塞。 - reddish
我在我的问题中非常明确地表示我不想这样做。 - reddish
我相信你说过:“我想要能够正确地终止/加入线程,因为在我正在解决的实际情况中,我想要优雅地终止网络连接。”你有很多选项来终止你的网络连接...将此线程设置为守护进程并不会阻止你加入它... - Bi Rico
我很感谢您的建议,但它并没有确切地回答我的问题,即:“在关闭交互式Python会话时结束非守护线程”。 - reddish
就算这并不是正解,但我还是标记了你的回答为有用。它提供了一种替代方案。 - reddish
显示剩余6条评论

0

只需在后台创建一个新的关闭线程:

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)
        print("shutting down.")
    def request_quit(self):
        self._quit_flag = True

mt = MyThread()
mt.start()

# Shutdown thread
threading.Thread(
    target=lambda: threading.main_thread().join() or mt.request_quit()
    ).start()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接