• Duda con manejo de hilos en python

 #459045  por azav3
 02 Nov 2014, 20:11
He escuchado que en Python el manejo de hilos es un poco enredado y que tampoco se lleva muy bien con tkinter, el problema que tengo es el siguiente: tengo una clase para la interfaz, y otra clase para un proceso cualquiera (es un proceso que se queda en un while infinito). Inicio primero la clase de la interfaz y luego la clase con el proceso infinito. Luego, mi idea es que cuando se cierre la interfaz, también se termine el proceso infinito y el programa termine. El código simplificado sería así:
import time, threading
from tkinter import *
from tkinter import messagebox

finalizar = False

class Interfaz(threading.Thread):
	def __init__(self):
		threading.Thread.__init__(self)

	def run(self):	
		global finalizar
		#Main Window
		self.mainWindow = Tk()
		self.mainWindow.geometry("200x200")
		self.mainWindow.title("My GUI Title")
		#Label
		lbCommand = Label(self.mainWindow, text="Hola mundo", font=("Courier New", 16)).place(x=20, y=20)
		#Start
		self.mainWindow.mainloop()
		#Cuando se cierre la GUI seteamos finalizar a True
		finalizar = True

class ClaseDos(threading.Thread):
	def __init__(self):
		threading.Thread.__init__(self)
	
	def run(self):
		global finalizar
		while not finalizar:
			print("Loop")
			time.sleep(3)
							
GUI = Interfaz()
GUI.start()
Clase = ClaseDos()
Clase.start()
Cuando hago click en el botón cerrar (en la esquina superior derecha de la GUI) me lanza el siguiente error:
Tcl_AsyncDelete: async handler deleted by the wrong thread
No logro entender por qué este error, y en Google no encuentro mucha información útil. ¿Alguien sabe a qué se debe?

¡Saludos!
 #459139  por sanko
 04 Nov 2014, 19:13
Creo que lo que quieres puede hacerse de 3 formas, por un método no documentado y oculto "_stop()", mediante el método join() y poniendo un daemon en estado verdadero, intente probarlo ahora rapido en tu clase y no pude implementarlo bien, luego con tiempo a la vuelta lo miro detenidamente.

De todas formas yo usaria processing en lugar de threading y en cuanto a la forma en la que creas los hilos, yo casi que haria una clase para la interfaz sin más, otra para ese bucle infinito y otra para gestionar los hilos, inicializarlos y detenerlos, puesto que el principal problema que surge en tu script a la hora de cerrar los hilos es que no lo hacen en el orden que deberian y gestionandolo desde un tercero podriamos aprovechar el "threading.Thread(target=method)" para lanzarlos, inicializarlos y detenerlos
 #459144  por sanko
 04 Nov 2014, 20:00
no podia con la intriga, asi que antes de ir pal gimnasio me hice algo así:
import time, threading
from Tkinter import *


class Interfaz():
	def __init__(self):
		self.mainWindow = Tk()
		self.mainWindow.geometry("200x200")
		self.mainWindow.title("My GUI Title")
		lbCommand = Label(self.mainWindow, text="Hola mundo", font=("Courier New", 16)).place(x=20, y=20)
		self.mainWindow.mainloop()
		
        
class ClaseDos():
	def __init__(self):
		while not finalizar:
			print "Loop"
			time.sleep(1)


class ThreadManager():
	global finalizar
	finalizar = False

	hilo = threading.Thread(target=ClaseDos)
	hilo.start()
	
	Interfaz()
	finalizar = True
	hilo.join()


ThreadManager()
Si quieres que la clase Interfaz sea un hilo tambien, haces lo mismo que con la otra, el problema es que si creas el hilo entonces el bucle infinito solo se ejecutaria una vez, una por hilo vaya, pero bueno es retocable, creo que es algo asi lo que buscabas

PD: es python2 pero vaya, que cambia el print y la forma de importar Tkinter
 #459152  por sanko
 04 Nov 2014, 21:17
Pues nada colega, al lanzar el otro hilo sale ese error que comentas otra vez :S
Si ves la forma de resolverlo comenta porque ahora me has dejao dudoso y no dejo de pensar en ello xd
 #459159  por Metal_Kingdom
 05 Nov 2014, 02:04
Ya sabéis que no soy de python, pero a ver si esto os ayuda (funciona bien):
import time, threading
from tkinter import *
from tkinter import messagebox

class App(object):
    def __init__(self, master):
        master.geometry("200x200")
        master.title("My GUI Title")
        lbCommand = Label(master, text="Hola mundo", font=("Courier New", 16)).place(x=20, y=20)

def InfiniteProcess():
    while not finalizar:
        print("Loop")
        time.sleep(3)

finalizar = False
Process = threading.Thread(target=InfiniteProcess)
Process.start()

mainWindow = Tk()
app = App(mainWindow)
mainWindow.mainloop()
#When the GUI is closed we set finish to "True"
finalizar = True
Process.join()
Explicación: [ Debe registrarse para ver este enlace ]

Saludos!
 #459167  por azav3
 05 Nov 2014, 05:33
Muchas gracias por su tiempo y sus respuestas.

Vamos por partes.

Sanko , el método threadX.join() lo que hace es detener la ejecución hasta que el threadX termine y pese a que he intentado darle una utilidad no sirve de mucho así como para terminar un thread. Lo segundo que mencionaste fue lo de crear una nueva clase para inicializar y detener los threads, no es una mala idea, de hecho el código se ve más ordenado pero en realidad es lo mismo que inicializarlos desde el MainThread .

Metal, muchas gracias pero la verdad es que ese tema que encontraste en StackOverflow lo abrí yo, y de hecho dice exactamente lo mismo que este post sólo que traducido en inglés.

Lo que me dice un tipo ahí en ese post es que al parecer tkinter es dependiente de Tcl y todos los comandos de Tcl deben originarse desde el mismo thread. Cuando nosotros inicializamos la clase Interfaz desde el MainThread (o desde el ThreadManager en tu caso) entonces todos los comandos de esa clase (y el uso de sus métodos y atributos) deben ejecutarse en ese mismo thread. Cuando la clase Interfaz se termina, el atributo self.mainWindow es destruido en el thread GUI y no en el thread que se originó, por eso surge el error. Una solución es simplemente no hacer el mainWindow un atributo de la clase (reemplazar todos los self.mainWindow por mainWindow en el código original) y eso efectivamente funciona.

Yo le respondí al tipo que en mi caso real el self.mainWindow DEBÍA ser un atributo de la clase para que funcionara y por eso me da otras soluciones, sin embargo todas las otras soluciones que me da (dentro de ellas la que puso Metal) implican transformar una de las clases a una función (o no usar dos clases), pero en el problema real yo DEBO usar dos clases porque cada una tiene métodos y atributos propios que se usan a lo largo de todo el código.

Todo esto último se lo planteé al tipo así es que ahora tengo que esperar a ver que me responde. Además eso que me dijo de que los comandos Tcl deben surgir todos del mismo thread no se si es la razón verdadera por la cual viene el problema y según varias pruebas que hice que no creo que ese sea el problema. También se lo planteé a ver que opina. Si entiendes inglés puedes seguir toda la conversación en el post original: [ Debe registrarse para ver este enlace ]