在 python 中终止计时器线程的正确方法是什么?

What is the proper way to terminate a Timer thread in python?

我能找到的关于终止 Python 计时器线程的唯一其他问题是这个问题: 但这只告诉我阅读线程文档时我已经知道的内容。

我正在 Python 中编写一个 GTK 应用程序。它通过 d-bus 与 HexChat 连接。因为当用户更改上下文(切换到不同的 IRC 频道或服务器 window)时没有 D-bus 信号,所以我的原始设计等到用户以某种方式与 GUI 交互以查看我们当前所处的上下文. 这样做的缺点是需要切换的输入框不能真正用于切换上下文,因为如果用户开始输入,则会发出更改的信号并将内容保存到旧上下文中,这是不正确的行为.

为了解决这个问题,我决定使用线程计时器任务每 0.5 秒检查一次当前上下文是什么。线程函数检查当前上下文,更新 class 中的变量,然后再延迟 0.5 秒重新启动。这非常有效,而且速度足够快,可以跟上用户经常切换频道的速度。

然而,即使我将 TimerThread.cancel 添加到我的 __del__() 函数,我的程序并没有退出,它只是挂起,直到我发出键盘中断。我还尝试再次执行 TimerThread.cancel, sleep(0.1), TimerThread.cancel 以防万一我碰巧取消了计时器,因为它在上下文检查功能中。同样的结果。我还尝试将 TimerThread.cancel 放在 onDestroy 函数中(这将依次调用 __del__ 析构函数)但没有改变。 GTK 循环退出,GUI 消失,但程序只是挂在控制台中,直到键盘中断。当我 do 发出键盘中断时,我从线程库中得到一个回溯错误。

有没有其他方法可以终止线程?在退出之前,我还需要采取其他步骤来终止线程库吗?我是不是误解了定时器线程的工作原理?

以下是代码的相关部分: https://paste.ubuntu.com/p/kQVfF78H5R/

编辑: 这是回溯,如果有帮助的话。

^CException ignored in: <module 'threading' from '/usr/lib/python3.7/threading.py'>
Traceback (most recent call last):
  File "/usr/lib/python3.7/threading.py", line 1281, in _shutdown
    t.join()
  File "/usr/lib/python3.7/threading.py", line 1032, in join
    self._wait_for_tstate_lock()
  File "/usr/lib/python3.7/threading.py", line 1048, in _wait_for_tstate_lock
    elif lock.acquire(block, timeout):
KeyboardInterrupt

我认为您 运行 处于竞争状态。尝试用锁保护 contextUpdater 变量。在这里,我添加了三个新函数,即从 class __init__ 方法调用的 start_updaterstop_updater 和从 [=] 调用的 restart_updater 17=] 方法。在退出您的应用程序之前,调用 stop_updater 方法将取消当前 运行 计时器(不要从 __del__ 方法调用它):

    def __init__(self):
        self._lock = threading.Lock()
        self.start_updater()

    def start_updater(self):
        with self._lock:
            self._contextUpdater = threading.Timer(0.5, self.getContextTimer)
            self._contextUpdater.start()
            self._running = True

    def stop_updater(self):
        with self._lock:
            if self._contextUpdater.is_alive():
                self._contextUpdater.cancel()
            self._running = False

    def restart_updater(self):
        with self._lock:
            if not self._running:
                return
        self.start_updater()        

    def getContextTimer(self):
        context = self.hcInterface.FindContext('', '')
        try:
            curChan = self.contextByID[int(context)]
        except KeyError:
            self.restart_updater()
            return # we are in a context we don't care about
        if curChan in self.inviteChannels:
            self.getContext(None, 0)
        elif curChan in self.supportChannels:
            self.getContext(None, 1)
        else:
            self.getContext(None)
        self.restart_updater()

我用以下玩具示例测试了这种方法:

class FooTimer(object):
    def __init__(self):
        self._lock = Lock()
        self.start()

    def start(self):

        with self._lock:
            self._timer = Timer(1, self.update)
            self._timer.start()
            self._running = True

    def stop(self):

        with self._lock:

            if self._timer.is_alive():
                self._timer.cancel()

            self._running = False

    def restart(self):

        with self._lock:

            if not self._running:
                return

        self.start()

    def update(self):
        print('tick')
        self.restart()