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

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


Your theory sounds good to me. You don't get an error or warning calling .destroy on a previously destroyed Toplevel window because Tkinter "helpfully" doesn't complain about that. :)

Here's a version of your code that seems to work, at least it doesn't leave unwanted windows lying around. I got rid of that global, and push the windows onto a stack so I can pop them when I want to destroy them. In your real code you probably want to loop over the stack and check the window id's so you destroy the right one.

import tkinter as tkimport queueimport threadingimport timewindow_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 queuedef 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 regularlyapp_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()

Uncomment this line:

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

to demonstrate that destroying a window twice produces no error message.


My solution that appears to work is to use locks. I acquire a lock before I send the message to the queue that tells the main thread to create the toplevel. After the main thread has created the toplevel, it releases the lock.

Now before I send the message to destroy the toplevel it, I acquire the lock again which will block until the main thread has finished creating it.

import tkinter as tkimport queueimport threadingimport timedef 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 queuedef 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 regularlyapp_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()

Another (probably more simple) solution that I came up with was to create the window on the main thread after the button was pressed, which will prevent the thread from being started until the window has been created:

import tkinter as tkimport queueimport threadingimport timedef 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 queuedef 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 regularlyapp_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()