关闭 tkinter 应用程序时,Python 线程调用不会完成

wim*_*rks 5 python multithreading tkinter python-3.x

我正在 python 中使用 tkinter 制作一个计时器。该小部件只有一个按钮。该按钮兼作显示剩余时间的元素。计时器有一个线程,可以简单地更新按钮上显示的时间。

该线程仅使用一个 while 循环,该循环应在设置事件时停止。当窗口关闭时,我使用协议调用设置此事件的函数,然后尝试加入线程。这在大多数情况下都有效。但是,如果我在进行某个调用时关闭程序,则会失败并且线程在窗口关闭后继续运行。

我知道有关关闭 tkinter 窗口时关闭线程的其他 类似线程。但这些答案已经过时了,如果可能的话,我想避免使用 thread.stop() 。

我尝试尽可能减少这一点,同时仍然表明我对该计划的意图。

import tkinter as tk
from tkinter import TclError, ttk
from datetime import timedelta
import time
import threading
from threading import Event

def strfdelta(tdelta):
    # Includes microseconds
    hours, rem = divmod(tdelta.seconds, 3600)
    minutes, seconds = divmod(rem, 60)
    return str(hours).rjust(2, '0') + ":" + str(minutes).rjust(2, '0') + \
           ":" + str(seconds).rjust(2, '0') + ":" + str(tdelta.microseconds).rjust(6, '0')[0:2]

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.is_running = False
        is_closing = Event()
        self.start_time = timedelta(seconds=4, microseconds=10, minutes=0, hours=0)
        self.current_start_time = self.start_time
        self.time_of_last_pause = time.time()
        self.time_of_last_unpause = None
        # region guisetup
        self.time_display = None
        self.geometry("320x110")
        self.title('Replace')
        self.resizable(False, False)
        box1 = self.create_top_box(self)
        box1.place(x=0, y=0)
        # endregion guisetup
        self.timer_thread = threading.Thread(target=self.timer_run_loop, args=(is_closing, ))
        self.timer_thread.start()

        def on_close():  # This occasionally fails when we try to close.
            is_closing.set()  # This used to be a boolean property self.is_closing. Making it an event didn't help.
            print("on_close()")
            try:
                self.timer_thread.join(timeout=2)
            finally:
                if self.timer_thread.is_alive():
                    self.timer_thread.join(timeout=2)
                    if self.timer_thread.is_alive():
                        print("timer thread is still alive again..")
                    else:
                        print("timer thread is finally finished")
                else:
                    print("timer thread finished2")
            self.destroy()  # /sf/ask/7780881/
        self.protocol("WM_DELETE_WINDOW", on_close)

    def create_top_box(self, container):
        box = tk.Frame(container, height=110, width=320)
        box_m = tk.Frame(box, bg="blue", width=320, height=110)
        box_m.place(x=0, y=0)
        self.time_display = tk.Button(box_m, text=strfdelta(self.start_time), command=self.toggle_timer_state)
        self.time_display.place(x=25, y=20)
        return box

    def update_shown_time(self, time_to_show: timedelta = None):
        print("timer_run_loop must finish. flag 0015")  # If the window closes at this point, everything freezes
        self.time_display.configure(text=strfdelta(time_to_show))
        print("timer_run_loop must finish. flag 016")

    def toggle_timer_state(self):
        # update time_of_last_unpause if it has never been set
        if not self.is_running and self.time_of_last_unpause is None:
            self.time_of_last_unpause = time.time()
        if self.is_running:
            self.pause_timer()
        else:
            self.start_timer_running()

    def pause_timer(self):
        pass  # Uses self.time_of_last_unpause, Alters self.is_running, self.time_of_last_pause, self.current_start_time

    def timer_run_loop(self, event):
        while not event.is_set():
            if not self.is_running:
                print("timer_run_loop must finish. flag 008")
                self.update_shown_time(self.current_start_time)
            print("timer_run_loop must finish. flag 018")
        print("timer_run_loop() ending")

    def start_timer_running(self):
        pass  # Uses self.current_start_time; Alters self.is_running, self.time_of_last_unpause

if __name__ == "__main__":
    app = App()
    app.mainloop()

Run Code Online (Sandbox Code Playgroud)

你甚至不必按下按钮,这个错误就会显现出来,但它确实需要尝试和错误。我只是运行它并按 alt f4 直到它发生。

如果您运行此程序并遇到问题,您将看到“timer_run_loop必须完成。flag 0015”是我们检查线程是否结束之前打印的最后一个内容。也就是说, self.time_display.configure(text=strfdelta(time_to_show))还没有结束。我认为当线程在其中使用此 tkinter 按钮时关闭 tkinter 窗口会以某种方式导致问题。

关于 tkinter 中的配置方法似乎很少有可靠的文档。Python 的tkinter 官方文档只是顺便提到了该函数。它只是用作只读字典。
tkinter 样式类获取有关其配置方法的一些详细信息,但这没有帮助。
tkdocs配置(又名 config)列为可用于所有小部件的方法之一。
教程文章似乎是唯一展示实际使用的功能的地方。但它没有提及该方法可能遇到的任何可能的问题或异常。

是否有一些我没有使用的资源共享模式?或者有更好的方法来结束这个线程吗?

Col*_*axd 2

好的,所以,首先我想介绍一下.after方法,它可以与你的widget结合使用,不需要使用线程

请注意,该函数被调用一次并再次调用自身,使得循环通常update_time不会干扰 的主循环。tkinter这将与程序一起关闭,没有任何问题。

import datetime
from tkinter import *

start_time = datetime.datetime.now()


def update_timer():
   current_time = datetime.datetime.now()
   timer_label.config(text=f'{current_time - start_time}')
   root.after(1000, update_timer)


root = Tk()
timer_label = Label(text='0')
timer_label.pack()

update_timer()
root.mainloop()
Run Code Online (Sandbox Code Playgroud)

现在关于非守护线程的一些解释...当您创建一个非守护线程时,它会一直运行直到执行完毕,也就是说,即使parent关闭它也会保持打开状态,直到进程结束。

# import module
from threading import *
import time

# creating a function
def thread_1():             
    for i in range(5):
        print('this is non-daemon thread')
        time.sleep(2)

# creating a thread T
T = Thread(target=thread_1)

# starting of thread T
T.start()   

# main thread stop execution till 5 sec.
time.sleep(5)               
print('main Thread execution')
Run Code Online (Sandbox Code Playgroud)

输出

this is non-daemon thread
this is non-daemon thread
this is non-daemon thread
main Thread execution
this is non-daemon thread
this is non-daemon thread
Run Code Online (Sandbox Code Playgroud)

现在看使用守护线程的相同示例,该线程将尊重主线程的执行性质,也就是说,如果它停止,“子线程”也会停止。

this is non-daemon thread
this is non-daemon thread
this is non-daemon thread
main Thread execution
this is non-daemon thread
this is non-daemon thread
Run Code Online (Sandbox Code Playgroud)
this is thread T
this is thread T
this is Main Thread
Run Code Online (Sandbox Code Playgroud)

也就是说,我的主要解决方案是使用.after,如果即使这个解决方案失败并且您需要使用线程,使用线程daemon=True,这将在您关闭应用程序后正确关闭线程tkinter

您可以查看有关 Python 守护线程的更多信息

  • 很好的答案,但它没有解释为什么会发生问题,即使线程不是“守护进程”。为什么设置 `threading.Event` 标志后主线程不退出?也不是很重要,但不鼓励使用“from tkinter import *”。最好使用“import tkinter as tk”。 (2认同)