web-dev-qa-db-fra.com

Comment arrêter un thread en boucle en Python?

Quelle est la bonne façon de dire à un thread en boucle d'arrêter de boucler?

J'ai un programme assez simple qui envoie une commande ping à un hôte spécifié dans une classe séparée threading.Thread. Dans cette classe, il dort 60 secondes, il s'exécute de nouveau jusqu'à la fermeture de l'application.

Je voudrais implémenter un bouton 'Stop' dans mon wx.Frame pour demander au thread en boucle de s'arrêter. Il n'est pas nécessaire de terminer le fil immédiatement, il peut simplement arrêter de boucler une fois qu'il est réveillé.

Voici ma classe threading (note: je n'ai pas encore implémenté la mise en boucle, mais cela tomberait probablement sous la méthode run dans 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)

Et dans mon cadre wxPyhton, cette fonction est appelée à partir d'un bouton Démarrer:

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

Fonction d'arrêt taraudée

Au lieu de sous-classer threading.Thread, on peut modifier la fonction pour permettre l’arrêt par un drapeau.

Nous avons besoin d'un objet, accessible à la fonction en cours d'exécution, sur lequel nous définissons l'indicateur pour qu'il cesse de courir.

Nous pouvons utiliser l'objet 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 __== "__main__":
    main()

L'astuce est que le thread en cours d'exécution peut avoir des propriétés supplémentaires attachées. La solution repose sur des hypothèses:

  • le thread a une propriété "do_run" avec la valeur par défaut True
  • le processus parent qui conduit peut affecter au thread démarré la propriété "do_run" à False.

En exécutant le code, nous obtenons la sortie suivante:

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

Pilule à tuer - en utilisant l'événement

Une autre alternative consiste à utiliser threading.Event comme argument de fonction. Il s'agit par défaut de False, mais un processus externe peut le "régler" (sur True) et la fonction peut en apprendre davantage à l'aide de la fonction wait(timeout).

Nous pouvons wait avec zéro délai d'attente, mais nous pouvons également l'utiliser comme minuteur en veille (utilisé ci-dessous).

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: J'ai essayé ceci dans Python 3.6. stop_event.wait() bloque l'événement (et donc la boucle while) jusqu'à sa libération. Il ne renvoie pas de valeur booléenne. Utiliser stop_event.is_set() fonctionne à la place.

Arrêter plusieurs filets avec une seule pilule

On voit mieux l'avantage de la pilule à tuer, s'il faut arrêter plusieurs threads à la fois, car une pilule fonctionnera pour tous.

La doit ne changera pas du tout, seule la main gérera les threads un peu différemment.

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()
78
Jan Vlcinsky

Cela a été demandé auparavant sur Stack. Voir les liens suivants:

En gros, il vous suffit de configurer le thread avec une fonction stop qui définit une valeur sentinel que le thread va vérifier. Dans votre cas, vous devez demander à quelque chose dans votre boucle de vérifier la valeur de sentinelle pour voir si elle a changé et si c'est le cas, la boucle peut se rompre et le fil peut mourir.

25
Mike Driscoll

J'ai lu les autres questions sur Stack mais j'étais toujours un peu confus de communiquer entre classes. Voici comment je l'ai abordé:

J'utilise une liste pour contenir tous mes threads dans la méthode __init__ de ma classe wxFrame: self.threads = []

Comme recommandé dans Comment arrêter un thread en boucle dans Python? J'utilise un signal dans ma classe de thread qui est défini sur True lors de l'initialisation de la classe de thread.

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

et je peux arrêter ces discussions en parcourant mes discussions:

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

J'ai eu une approche différente. J'ai sous-classé une classe Thread et dans le constructeur, j'ai créé un objet Event. Ensuite, j'ai écrit la méthode join () personnalisée, qui définit d'abord cet événement, puis appelle sa version parent.

Voici ma classe que j'utilise pour la communication par port série dans l'application 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 file d'attente est utilisée pour envoyer des messages au port et la boucle principale récupère les réponses. Je n'ai pas utilisé de méthode serial.readline (), en raison de caractères différents en fin de ligne, et j'ai constaté que l'utilisation des classes io était trop compliquée.

1
Piotr Sawicki