Comportement des Threads Principaux et Secondaires en Python

Lorsqu'un processus principal se termine avant ses threads secondaires, le comportement dépend du système d'exploitation. Sous Linux, le noyau réattribue le thread secondaire à l'init (processus avec PID 1), qui se chargera de sa gestion et de sa récupération une fois terminé. Si le processus principal n'est pas encore terminé, les threads secondaires continuent leur exécution normalement. Cepenadnt, si le processus principal se termine, tous ses threads sont également arrêtés.

Par défaut, les threads créés par le thread principal ne sont pas des threads "démon" (daemon threads). Cela signifie que le thread principal attendra la fin de l'exécution de tous les threads secondaires avant de terminer le processus. Tous les threads partagent le même terminal pour la sortie.

Voici un exemple illustrant ce comportement :


import threading
import time

def thread_secondaire_un():
    for _ in range(100):
        time.sleep(1)
        print('Exécution du thread secondaire un...')

def thread_principal():
    print('Exécution du thread principal...')
    thread1 = threading.Thread(target=thread_secondaire_un)
    thread1.start()
    print('Fin du thread principal...')

if __name__ == "__main__":
    thread_principal()

La sortie sera la suivante :


Exécution du thread principal...
Fin du thread principal...
Exécution du thread secondaire un...
Exécution du thread secondaire un...
Exécution du thread secondaire un...
Exécution du thread secondaire un...
...

Comme on peut le constater, le thread secondaire continue de s'exécuter après la fin du thread principal. Le processus ne se termine que lorsque tous les threads secondaires ont fini.

Threads Démon : Fin Immédiate du Processus

Un thread marqué comme démon (daemon thread) n'oblige pas le processus principal à attendre sa complétion. Le processus se terminera dès que tous les threads non-démons auront terminé, même si des threads démons sont encore en cours d'exécution.

Considérez l'exemple suivant où un thread est configuré comme démon :


import threading
import time

def thread_secondaire_un():
    for _ in range(100):
        time.sleep(1)
        print('Exécution du thread secondaire un...')

def thread_secondaire_deux():
    for _ in range(5):
        time.sleep(1)
        print('Exécution du thread secondaire deux...')

def thread_principal():
    print('Exécution du thread principal...')
    thread1 = threading.Thread(target=thread_secondaire_un)
    thread2 = threading.Thread(target=thread_secondaire_deux)

    thread1.setDaemon(True) # thread1 est un thread démon
    thread1.start()
    thread2.start()

    print('Fin du thread principal...')

if __name__ == "__main__":
    thread_principal()

La sortie typique sera :


Exécution du thread principal...
Fin du thread principal...
Exécution du thread secondaire un...Exécution du thread secondaire deux...
Exécution du thread secondaire un...Exécution du thread secondaire deux...
Exécution du thread secondaire un...Exécution du thread secondaire deux...
Exécution du thread secondaire un...Exécution du thread secondaire deux...
Exécution du thread secondaire un...Exécution du thread secondaire deux...

Process finished with exit code 0

Dans cet exemple, thread1 est un thread démon, tandis que thread2 ne l'est pas. Le processus attendra la fin de thread2 mais pas celle de thread1.

Note : Un thread secondaire hérite de la propriété démon de son thread parent. Si un thread démon crée un autre thread, ce derneir sera également un thread démon par défaut.

Attendre la Fin des Threads avec join()

La méthode join() permet à un thread d'attendre qu'un autre thread spécifique ait terminé son exécution. Par exemple, si le thread A appelle B.join(), le thread A sera bloqué à cet endroit et ne reprendra son exécution qu'une fois que le thread B aura terminé.

Voici une démonstration utilisant join() :


import threading
import time

def thread_secondaire_un():
    for _ in range(10):
        time.sleep(1)
        print('Exécution du thread secondaire un...')

def thread_secondaire_deux():
    for _ in range(5):
        time.sleep(1)
        print('Exécution du thread secondaire deux...')

def thread_principal():
    print('Exécution du thread principal...')
    thread1 = threading.Thread(target=thread_secondaire_un)
    thread2 = threading.Thread(target=thread_secondaire_deux)

    thread1.setDaemon(True)
    thread2.setDaemon(True)

    thread1.start()
    thread2.start()

    thread2.join() # Le thread principal attend la fin de thread2
    print("thread2 a terminé.")
    # Si une erreur survient avant thread1.join(), thread1 pourrait être interrompu si c'est un daemon thread
    # Dans cet exemple, une division par zéro entraînera la sortie du thread principal
    # et l'arrêt immédiat de thread1 s'il est un daemon thread.
    1/0 # Provocation d'une erreur pour démontrer le comportement
    thread1.join() # Cette ligne pourrait ne pas être atteinte
    print('Fin du thread principal...')

if __name__ == "__main__":
    try:
        thread_principal()
    except ZeroDivisionError:
        print("Erreur de division par zéro interceptée dans le thread principal.")

La sortie sera :


Exécution du thread principal...
Exécution du thread secondaire un...
Exécution du thread secondaire deux...
Exécution du thread secondaire un...
Exécution du thread secondaire deux...
Exécution du thread secondaire un...
Exécution du thread secondaire deux...
Exécution du thread secondaire un...
Exécution du thread secondaire deux...
Exécution du thread secondaire un...
Exécution du thread secondaire deux...
thread2 a terminé.
Erreur de division par zéro interceptée dans le thread principal.

Le thread principal est bloqué à thread2.join() jusqu'à ce que thread2 se termine. L'erreur de division par zéro provoque la sortie du thread principal. Comme thread1 est un thread démon, il est terminé immédiatement lorsque le processus principle se termine, et thread1.join() n'est jamais exécuté.

Étiquettes: threading Python daemon threads thread join

Publié le 7 juillet à 22h35