【问题标题】:What is the proper way to terminate a Timer thread in python?在 python 中终止 Timer 线程的正确方法是什么?
【发布时间】:2019-08-13 03:01:53
【问题描述】:

我能找到的关于终止 Python 计时器线程的唯一其他问题是这个:how to terminate a timer thread in python,但这只是告诉我从阅读线程文档中已经知道的内容。

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

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

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

还有其他方法可以终止线程吗?在退出之前我还需要采取其他步骤来杀死线程库吗?我是否误解了 Timer 线程的工作原理?

以下是代码的相关部分: 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

【问题讨论】:

  • 请将代码直接粘贴到帖子中。
  • @constt 过去有人告诉我,除了几行以外的任何内容都应该直接粘贴到帖子中。是哪个?
  • 根据rules,对你发的代码长度没有严格要求。如果您提供 live 示例,我认为在其他地方发布代码并链接到它是合理的。在其他情况下,最好将代码包含在问题中。

标签: python-3.x multithreading gtk


【解决方案1】:

我认为您遇到了竞争状况。尝试用锁保护contextUpdater 变量。在这里,我添加了三个新函数,分别是start_updater,从类__init__方法调用,stop_updater,和restart_updater,从getContextTimer方法调用。在退出您的应用程序之前,调用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()

【讨论】:

  • 太棒了!我认为通过睡眠时间少于计时器的长度,可以在取消时避免竞争条件,但使用 Lock 是一种更好的方法。非常感谢这个解决方案!
  • 刚刚在我的应用程序中测试了这个,它现在干净地退出了! StackOverflow 太棒了:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-02-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多