92 votes

Comment arrêter un thread en boucle en Python ?

Quelle est la bonne façon de dire à un fil qui tourne en boucle d'arrêter de tourner en boucle ?

J'ai un programme assez simple qui envoie un ping à un hôte spécifié dans un fichier séparé. threading.Thread classe. Dans cette classe, il se met en veille pendant 60 secondes, puis se remet en marche jusqu'à ce que l'application quitte.

J'aimerais mettre en place un bouton "Stop" dans mon système de gestion de l'information. wx.Frame pour demander au fil en boucle de s'arrêter. Il n'est pas nécessaire de terminer le fil immédiatement, il peut simplement arrêter de tourner en boucle dès qu'il se réveille.

Voici mon threading (note : je n'ai pas encore implémenté le bouclage, mais cela relèverait probablement de 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, j'ai cette fonction appelée depuis un bouton de démarrage :

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

151voto

Jan Vlcinsky Points 7932

Fonction d'arrêt du filetage

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 d'exécution, auquel nous mettons le drapeau pour arrêter l'exécution.

Nous pouvons utiliser threading.currentThread() objet.

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

if __name__ == "__main__":
    main()

Le truc, c'est que le fil d'exécution peut avoir des propriétés supplémentaires attachées. La solution repose sur sur des hypothèses :

  • le thread a une propriété "do_run" avec une valeur par défaut True
  • le processus parent conducteur peut assigner au thread démarré la propriété "do_run" pour 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 pour tuer - utilisation de l'événement

Une autre alternative est d'utiliser threading.Event comme argument de fonction. Il est par défaut False mais un processus externe peut le "fixer" (pour True ) et la fonction peut en apprendre davantage à ce sujet en utilisant wait(timeout) fonction.

Nous pouvons wait avec un délai d'attente de zéro, mais nous pouvons également l'utiliser comme une minuterie de sommeil (utilisée 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'à la libération. Elle ne renvoie pas de valeur booléenne. Utilisation de stop_event.is_set() fonctionne à la place.

Arrêter plusieurs fils avec une seule pilule

L'avantage de la pilule pour tuer est mieux vu, si nous devons arrêter plusieurs fils. à la fois, car une seule pilule fonctionnera pour tous.

En doit ne changera pas du tout, seul le main gère les fils 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()

29voto

Mike Driscoll Points 18662

Cette question a déjà été posée sur Stack. Voir les liens suivants :

En fait, il suffit de configurer le fil avec une fonction d'arrêt qui définit une valeur sentinelle que le fil vérifiera. Dans votre cas, quelque chose dans votre boucle vérifiera la valeur sentinelle pour voir si elle a changé et si c'est le cas, la boucle peut s'interrompre et le fil peut mourir.

14voto

multiphrenic Points 731

J'ai lu les autres questions sur Stack mais j'étais encore un peu confus sur la communication entre les classes. Voici comment j'ai abordé la question :

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

Comme recommandé dans Comment arrêter un thread en boucle en Python ? J'utilise un signal dans ma classe de fil qui est réglé sur True lors de l'initialisation de la classe de 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()

et je peux arrêter ces fils en itérant sur mes fils :

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

3voto

Piotr Sawicki Points 45

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

Voici ma classe, que j'utilise pour la communication par port série dans une 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é la méthode serial.readline(), à cause des différents caractères de fin de ligne, et j'ai trouvé que l'utilisation des classes io était trop compliquée.

3voto

kolypto Points 3161

Ça dépend de ce que vous faites dans ce fil. Si c'est votre code, alors vous pouvez implémenter une condition d'arrêt (voir les autres réponses).

Cependant, si ce que vous voulez est d'exécuter le code de quelqu'un d'autre, alors vous devez bifurquer et démarrer un processus. Comme ceci :

import multiprocessing
proc = multiprocessing.Process(target=your_proc_function, args=())
proc.start()

Maintenant, quand vous voulez arrêter ce processus, envoyez-lui un SIGTERM comme ceci :

proc.terminate()
proc.join()

Et ce n'est pas lent : quelques fractions de seconde. Profitez-en :)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X