¿Cómo detener un hilo de bucle en Python?

¿Cuál es la forma correcta de decirle a un subproceso de bucle que detenga el bucle?

Tengo un progtwig bastante simple que hace ping a un host específico en una clase threading.Thread separada. En esta clase duerme 60 segundos, se ejecuta de nuevo hasta que la aplicación se cierra.

Me gustaría implementar un botón ‘Detener’ en mi wx.Frame para pedirle al subproceso de bucle que se detenga. No necesita terminar el hilo de inmediato, solo puede detener el bucle una vez que se despierta.

Aquí está mi clase de threading (nota: no he implementado bucles todavía, pero es probable que caiga bajo el método de ejecución en PingAssets)

 class PingAssets(threading.Thread): def __init__(self, threadNum, asset, window): threading.Thread.__init__(self) self.threadNum = threadNum self.window = window self.asset = asset def run(self): config = controller.getConfig() fmt = config['timefmt'] start_time = datetime.now().strftime(fmt) try: if onlinecheck.check_status(self.asset): status = "online" else: status = "offline" except socket.gaierror: status = "an invalid asset tag." msg =("{}: {} is {}. \n".format(start_time, self.asset, status)) wx.CallAfter(self.window.Logger, msg) 

Y en mi marco wxPyhton tengo esta función llamada desde un botón de Inicio:

 def CheckAsset(self, asset): self.count += 1 thread = PingAssets(self.count, asset, self) self.threads.append(thread) thread.start() 

Esto ha sido preguntado antes en Stack. Vea los siguientes enlaces:

  • ¿Hay alguna forma de matar un hilo en Python?
  • Detener un hilo después de un cierto tiempo

Básicamente, solo necesita configurar el subproceso con una función de parada que establece un valor de centinela que el hilo verificará. En su caso, tendrá algo en su bucle que verifica el valor del centinela para ver si ha cambiado y si lo ha hecho, el bucle se puede romper y el hilo puede morir.

Función stoppable roscada

En lugar de crear threading.Thread de threading.Thread . threading.Thread , se puede modificar la función para permitir detener una marca.

Necesitamos un objeto, accesible a la función de ejecución, a la que configuramos la bandera para que deje de ejecutarse.

Podemos usar el objeto threading.currentThread() .

 import threading import time def doit(arg): t = threading.currentThread() while getattr(t, "do_run", True): print ("working on %s" % arg) time.sleep(1) print("Stopping as you wish.") def main(): t = threading.Thread(target=doit, args=("task",)) t.start() time.sleep(5) t.do_run = False t.join() if __name__ == "__main__": main() 

El truco es que el hilo en ejecución puede tener propiedades adicionales adjuntas. La solución se basa en suposiciones:

  • el hilo tiene una propiedad “do_run” con el valor predeterminado True
  • el proceso principal de conducción puede asignar a la propiedad “do_run” iniciada como False .

Ejecutando el código, obtenemos la siguiente salida:

 $ python stopthread.py working on task working on task working on task working on task working on task Stopping as you wish. 

Píldora para matar – usando Evento

Otra alternativa es usar threading.Event como argumento de la función. Es False forma predeterminada, pero el proceso externo puede “configurarlo” (en True ) y la función puede aprender sobre él usando la función de wait(timeout) .

Podemos wait sin tiempo límite, pero también podemos usarlo como el temporizador para dormir (se usa a continuación).

 def doit(stop_event, arg): while not stop_event.wait(1): print ("working on %s" % arg) print("Stopping as you wish.") def main(): pill2kill = threading.Event() t = threading.Thread(target=doit, args=(pill2kill, "task")) t.start() time.sleep(5) pill2kill.set() t.join() 

Edit: probé esto en Python 3.6. stop_event.wait() bloquea el evento (y así el bucle while) hasta su liberación. No devuelve un valor booleano. Usar stop_event.is_set() funciona en su lugar.

Detener varios hilos con una pastilla

La ventaja de la píldora para matar se ve mejor si tenemos que detener varios hilos a la vez, ya que una píldora funcionará para todos.

El doit no cambiará en absoluto, solo el main maneja los hilos de manera un poco diferente.

 def main(): pill2kill = threading.Event() tasks = ["task ONE", "task TWO", "task THREE"] def thread_gen(pill2kill, tasks): for task in tasks: t = threading.Thread(target=doit, args=(pill2kill, task)) yield t threads = list(thread_gen(pill2kill, tasks)) for thread in threads: thread.start() time.sleep(5) pill2kill.set() for thread in threads: thread.join() 

Leí las otras preguntas en Stack, pero todavía estaba un poco confundido sobre la comunicación entre clases. Así es como lo abordé:

Uso una lista para mantener todos mis hilos en el método __init__ de mi clase self.threads = [] : self.threads = []

Como se recomienda en ¿Cómo detener un hilo de bucle en Python? Utilizo una señal en mi clase de hilo que se establece en True al inicializar la clase de hilo.

 class PingAssets(threading.Thread): def __init__(self, threadNum, asset, window): threading.Thread.__init__(self) self.threadNum = threadNum self.window = window self.asset = asset self.signal = True def run(self): while self.signal: do_stuff() sleep() 

y puedo detener estos hilos al iterar sobre mis hilos:

 def OnStop(self, e): for t in self.threads: t.signal = False 

Tuve un enfoque diferente. Subclasificé una clase Thread y en el constructor he creado un objeto de evento. Luego escribí el método join () personalizado, que primero establece este evento y luego llama a la versión de uno de los padres.

Aquí está mi clase, estoy usando para la comunicación del puerto serie en la aplicación wxPython:

 import wx, threading, serial, Events, Queue class PumpThread(threading.Thread): def __init__ (self, port, queue, parent): super(PumpThread, self).__init__() self.port = port self.queue = queue self.parent = parent self.serial = serial.Serial() self.serial.port = self.port self.serial.timeout = 0.5 self.serial.baudrate = 9600 self.serial.parity = 'N' self.stopRequest = threading.Event() def run (self): try: self.serial.open() except Exception, ex: print ("[ERROR]\tUnable to open port {}".format(self.port)) print ("[ERROR]\t{}\n\n{}".format(ex.message, ex.traceback)) self.stopRequest.set() else: print ("[INFO]\tListening port {}".format(self.port)) self.serial.write("FLOW?\r") while not self.stopRequest.isSet(): msg = '' if not self.queue.empty(): try: command = self.queue.get() self.serial.write(command) except Queue.Empty: continue while self.serial.inWaiting(): char = self.serial.read(1) if '\r' in char and len(msg) > 1: char = '' #~ print('[DATA]\t{}'.format(msg)) event = Events.PumpDataEvent(Events.SERIALRX, wx.ID_ANY, msg) wx.PostEvent(self.parent, event) msg = '' break msg += char self.serial.close() def join (self, timeout=None): self.stopRequest.set() super(PumpThread, self).join(timeout) def SetPort (self, serial): self.serial = serial def Write (self, msg): if self.serial.is_open: self.queue.put(msg) else: print("[ERROR]\tPort {} is not open!".format(self.port)) def Stop(self): if self.isAlive(): self.join() 

La cola se utiliza para enviar mensajes al puerto y el bucle principal recupera las respuestas. No he usado ningún método serial.readline () debido a las diferentes características de la línea final, y he encontrado que el uso de las clases io es demasiado complicado.