2016-11-02 4 views
3

Cela pourrait être la question la plus compliquée que j'ai posée ici. J'ai passé du temps à obtenir mon code le plus simple possible pour reproduire mon problème. J'espère que ce n'est pas trop compliqué pour obtenir de l'aide ...destroy() tkinter toplevel de la file d'attente échoue silencieusement (condition de concurrence?)

Fondamentalement, dans le code ci-dessous, une application tkinter avec un seul bouton est créée, et elle vérifie une file d'attente toutes les 100ms parce qu'un thread différent peut avoir besoin d'interagir avec elle plus tard. Une nouvelle fenêtre est également créée et détruite très rapidement parce que j'obtiens une erreur plus tard sinon (ceci peut être important)

Lorsque le bouton est cliqué, un nouveau fil est fait qui indique le fil principal (via la file d'attente) pour créer une fenêtre qui serait utilisée pour indiquer que quelque chose de potentiellement long est en train d'arriver, alors quand cela se termine, il indique au thread principal (via la file d'attente) de détruire la fenêtre. Le problème est que la fenêtre n'est pas détruite si la tâche de temps est très courte et il n'y a pas d'erreur non plus, mais si le processus dans le thread a pris beaucoup de temps (une seconde par exemple), cela fonctionne comme prévu . Je me demande si c'est quelque chose comme "Le nouvel objet fenêtre n'a pas encore été créé et affecté à new_window, alors quand j'ajoute la méthode destroy à la file d'attente, j'ajoute en fait l'ancien objet (précédemment détruit) méthode à la file ". Cela expliquerait pourquoi j'obtiens une erreur la première fois que je clique sur le bouton si je ne crée pas et ne détruis pas la fenêtre quand j'initialise l'application, mais cela n'explique pas pourquoi je n'obtiens pas d'erreur en appelant sur a détruit précédemment Toplevel ... Si je ne me trompe pas avec cette théorie, je ne sais pas vraiment ce que la solution est, donc toutes les idées seraient appréciés

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

BTW, c'est une question bien écrite. Si seulement plus de questions SO étaient si bien écrites, avec un beau MCVE ... :) –

Répondre

1

Ma solution qui semble fonctionner est de utiliser les verrous. J'acquiers un verrou avant d'envoyer le message à la file d'attente qui indique au thread principal de créer le toplevel. Après que le thread principal a créé le toplevel, il libère le verrou.

Maintenant, avant que j'envoie le message pour détruire le toplevel, j'acquiers à nouveau le verrou qui bloquera jusqu'à ce que le thread principal ait fini de le créer.

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

Une autre solution (probablement de plus simple) que je suis venu avec était de créer la fenêtre sur le thread principal après que le bouton a été pressé, ce qui empêchera le fil de démarrer jusqu'à ce que la fenêtre a été créé:

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

Votre théorie sonne bien pour moi. Vous n'obtenez pas d'erreur ou d'avertissement en appelant .destroy sur une fenêtre Toplevel précédemment détruite car Tkinter "utilement" ne s'en plaint pas. :)

Voici une version de votre code qui semble fonctionner, au moins, elle ne laisse pas de fenêtres indésirables. Je me suis débarrassé de cette global, et pousser les fenêtres sur une pile afin que je puisse les faire éclater quand je veux les détruire. Dans votre code réel, vous voulez probablement faire une boucle sur la pile et vérifier l'identifiant de la fenêtre pour détruire le bon.

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

Décommentez cette ligne:

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

pour démontrer que la destruction d'une fenêtre produit deux fois aucun message d'erreur.

+0

Votre code fonctionne, mais je suppose que c'est parce qu'il rend la condition de la concurrence moins probable parce qu'il doit maintenant faire face à une liste dans le deuxième thread, ce qui donne au thread principal assez de temps pour créer le toplevel. Est-ce qu'il me manque un blocage dans votre code qui empêche la destruction avant qu'elle ne soit finie? – Grezzo