来自队列的destroy()tkinter toplevel无声地失败(竞争条件?)

时间:2016-11-02 11:47:45

标签: python tkinter queue race-condition

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

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

当单击该按钮时,会创建一个新线程,告诉主线程(通过队列)创建一个窗口,用于指示可能耗时的事情,然后当完成时,它会告诉主线程(通过队列)来破坏窗口。

问题是如果耗时的任务非常短并且没有错误,则窗口不会被销毁,但如果线程中的进程花了很长时间(例如一秒),它就会按预期工作。

我想知道它是否是“新的窗口对象尚未创建并分配给new_window,所以当我将destroy方法添加到队列中时,我实际上是在添加旧的(之前已被破坏) )对象的破坏方法到队列“。这可以解释为什么我在第一次单击按钮时遇到错误,如果我在初始化应用程序时没有创建并销毁窗口,但它无法解释为什么我没有收到错误调用{{ 1}}以前被毁坏的destroy() ...如果我对这个理论是对的,我真的不知道解决方案是什么,所以任何想法都会受到赞赏

Toplevel

2 个答案:

答案 0 :(得分:1)

你的理论听起来不错。在先前销毁的Toplevel窗口上,您没有收到错误或警告.destroy因为Tkinter“有帮助”并没有抱怨。 :)

这是您的代码版本似乎有效,至少它不会留下不需要的窗口。我摆脱了那个global,并将窗户推到一个堆栈上,这样当我想要销毁它们时我可以弹出它们。在您的真实代码中,您可能希望循环堆栈并检查窗口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()

证明两次销毁窗口不会产生错误信息。

答案 1 :(得分:1)

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

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

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

我提出的另一个(可能更简单的)解决方案是在按下按钮后在主线程上创建窗口,这将阻止线程在窗口创建之前启动:

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