Cómo detener un hilo de bucle en Python?


¿Cuál es la forma correcta de decirle a un hilo de bucle que deje de hacerlo?

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

Me gustaría implementar un botón 'Stop' en mi wx.Frame para pedir que el hilo de bucle se detenga. No es necesario terminar el hilo de inmediato, solo puede dejar de girar una vez que se despierte.

Aquí está mi clase threading (nota: No he implementado looping todavía, pero probablemente caería bajo el método run 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()
Author: pedram, 2013-08-02

4 answers

Esto se ha preguntado antes en la pila. Vea los siguientes enlaces:

Básicamente solo necesita configurar el subproceso con una función stop que establezca un valor sentinel que el subproceso verificará. En tu caso, tendrás algo en tu bucle comprueba el valor de sentinel para ver si ha cambiado y si lo ha hecho, el bucle puede romperse y el hilo puede morir.

 21
Author: Mike Driscoll,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:26:03

Función tapable roscada

En lugar de subclase threading.Thread, se puede modificar la función para permitir pasando por una bandera.

Necesitamos un objeto, accesible para la función en ejecución, en el que establecemos 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 construye en supuestos:

  • el hilo tiene una propiedad "do_run" con valor predeterminado True
  • el proceso padre de conducción puede asignar al hilo iniciado la propiedad "do_run" a 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 función. Es por por defecto False, pero el proceso externo puede "establecerlo" (a True) y la función puede aprende sobre ello usando la función wait(timeout).

Podemos wait con cero tiempo de espera, pero también podemos usarlo como el temporizador para dormir (utilizado 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()

Editar: Probé esto en Python 3.6. stop_event.wait() bloquea el evento (y por lo tanto el bucle while) hasta el lanzamiento. No devuelve un valor booleano. Usar stop_event.is_set() funciona en su lugar.

Detener múltiples hilos con una píldora

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

El doit no cambiará en absoluto, solo el main maneja los hilos de forma 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()
 55
Author: Jan Vlcinsky,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-04-23 13:36:52

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 contener todos mis hilos en el método __init__ de mi clase wxFrame: self.threads = []

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

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 iterando sobre mis hilos:

def OnStop(self, e):
        for t in self.threads:
            t.signal = False
 10
Author: pedram,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:47:11

Yo tenía un enfoque diferente. He subclasificado una clase Thread y en el constructor he creado un objeto Event. Luego he escrito el método custom join (), que primero establece este evento y luego llama a la versión de sí mismo de un padre.

Aquí está mi clase, que estoy usando para la comunicación de 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 usa para enviar mensajes al puerto y el bucle principal recupera las respuestas. No he usado ninguna serie.método readline (), debido a diferentes caracteres de línea final, y he encontrado que el uso de las clases io es demasiado alboroto.

 0
Author: Piotr Sawicki,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-18 12:11:53