destroy() tkinter toplevel from queue 默默地失败(竞争条件?)

destroy() tkinter toplevel from queue fails silently (race condition?)

这可能是我在这里问过的最复杂的问题。我花了一些时间让我的代码尽可能简单,以重现我的问题。我希望得到任何帮助都不会太复杂...

基本上在下面的代码中,创建了一个带有单个按钮的 tkinter 应用程序,它每 100 毫秒检查一次队列,因为不同的线程稍后可能需要与之交互。一个新的 window 也很快被创建和销毁,因为我稍后会得到一个错误,否则 (这可能很重要)

单击按钮时,将创建一个新线程,告诉主线程(通过队列)创建一个 window 用于指示可能耗时的事情正在发生,然后当完成后,它告诉主线程(通过队列)销毁 window.

问题是window如果耗时很短也没有报错,但如果线程中的进程耗时较长(比如一秒),则window不会被销毁, 它按预期工作。

我想知道它是否类似于"The new window object hasn't been created and assigned to new_window yet, so when I add the destroy method to the queue, I am actually adding the old (previously destroyed) object's destroy method to the queue"。这可以解释为什么如果我在初始化应用程序时没有创建和销毁 window ,那么我第一次单击按钮时会出现错误,但这并不能解释为什么我没有得到在之前被破坏的 Toplevel 上调用 destroy() 时出错...如果我的理论是正确的,我真的不知道解决方案是什么,所以任何想法都会受到赞赏

import tkinter as tk
import queue
import threading
import time

def button_pressed():
    threading.Thread(target=do_something_on_a_thread).start()

def do_something_on_a_thread():
    global new_window
    app_queue.put(create_a_new_window)
    time.sleep(1)
    app_queue.put(new_window.destroy)

def create_a_new_window():
    global new_window
    new_window = tk.Toplevel()
    tk.Label(new_window, text='Temporary Window').grid()

#Check queue and run any function that happens to be in the queue
def check_queue():
    while not app_queue.empty():
        queue_item = app_queue.get()
        queue_item()
    app.after(100, check_queue)

#Create tkinter app with queue that is checked regularly
app_queue = queue.Queue()
app = tk.Tk()
tk.Button(app, text='Press Me', command=button_pressed).grid()
create_a_new_window()
new_window.destroy()
app.after(100, check_queue)
tk.mainloop()

你的理论对我来说听起来不错。你不会在之前销毁的 Toplevel window 上调用 .destroy 时收到错误或警告,因为 Tkinter "helpfully" 不会抱怨这一点。 :)

这是您的代码的一个版本,它似乎可以工作,至少不会留下不需要的 windows。我去掉了 global,并将 windows 压入堆栈,以便在我想销毁它们时弹出它们。在您的真实代码中,您可能希望遍历堆栈并检查 window id,以便销毁正确的 id。

import tkinter as tk
import queue
import threading
import time

window_stack = []

def destroy_top_window():
    print
    if window_stack:
        w = window_stack.pop()
        print('destroy', w, len(window_stack))
        w.destroy()
        #time.sleep(1); w.destroy()
    else:
        print('Stack empty!')

def button_pressed():
    threading.Thread(target=do_something_on_a_thread).start()

def do_something_on_a_thread():
    app_queue.put(create_a_new_window)
    time.sleep(1)
    app_queue.put(destroy_top_window)

def create_a_new_window():
    new_window = tk.Toplevel()
    tk.Label(new_window, text='Temporary Window').grid()
    window_stack.append(new_window)
    print('create ', new_window, len(window_stack))

#Check queue and run any function that happens to be in the queue
def check_queue():
    while not app_queue.empty():
        queue_item = app_queue.get()
        queue_item()
    app.after(100, check_queue)

#Create tkinter app with queue that is checked regularly
app_queue = queue.Queue()
app = tk.Tk()
tk.Button(app, text='Press Me', command=button_pressed).grid()

#create_a_new_window()
#destroy_top_window()

app.after(100, check_queue)
tk.mainloop()

取消注释这一行:

#time.sleep(1); w.destroy()

证明销毁 window 两次不会产生错误消息。

我的解决方案似乎有效使用锁。在将消息发送到告诉主线程创建顶层的队列之前,我获得了一个锁。主线程创建顶层后,释放锁。

现在,在我发送消息销毁顶层之前,我再次获取锁,这将阻塞直到主线程完成创建它。

import tkinter as tk
import queue
import threading
import time

def button_pressed():
    threading.Thread(target=do_something_on_a_thread).start()

def do_something_on_a_thread():
    global new_window
    my_lock.acquire()
    app_queue.put(create_a_new_window)
    my_lock.acquire()
    app_queue.put(new_window.destroy)

def create_a_new_window():
    global new_window
    new_window = tk.Toplevel()
    tk.Label(new_window, text='Temporary Window').grid()

#Check queue and run any function that happens to be in the queue
def check_queue():
    while not app_queue.empty():
        queue_item = app_queue.get()
        queue_item()
        my_lock.release()
    app.after(100, check_queue)

#Create tkinter app with queue that is checked regularly
app_queue = queue.Queue()
my_lock = threading.Lock()
app = tk.Tk()
tk.Button(app, text='Press Me', command=button_pressed).grid()
create_a_new_window()
new_window.destroy()
app.after(100, check_queue)
tk.mainloop()

我想出的另一个(可能更简单)解决方案是在按下按钮后在主线程上创建 window,这将阻止线程启动,直到 window 已创建:

import tkinter as tk
import queue
import threading
import time

def button_pressed():
    create_a_new_window()
    threading.Thread(target=do_something_on_a_thread).start()

def do_something_on_a_thread():
    global new_window
    app_queue.put(new_window.destroy)

def create_a_new_window():
    global new_window
    new_window = tk.Toplevel()
    tk.Label(new_window, text='Temporary Window').grid()

#Check queue and run any function that happens to be in the queue
def check_queue():
    while not app_queue.empty():
        queue_item = app_queue.get()
        queue_item()
    app.after(100, check_queue)

#Create tkinter app with queue that is checked regularly
app_queue = queue.Queue()
app = tk.Tk()
tk.Button(app, text='Press Me', command=button_pressed).grid()
create_a_new_window()
new_window.destroy()
app.after(100, check_queue)
tk.mainloop()