En Python, un Processus est l'unité de base de l'allocation des ressources et de l'ordonnancement du système d'exploitation, représentant une instance d'un programme Python en exécution. Chaque processus possède un espace mémoire indépendant, des descripteurs de fichiers, des variables globales et autres ressources. Par défaut, les processus sont isolés et ne peuvent pas partager directement de données, nécessitant des mécanismes spécifiques (IPC) pour communiquer.
Pourquoi Python a besoin du multiprocessus ?
Python est limité par le GIL (Global Interpreter Lock) : à un instant donné, un interpréteur Python ne peut exécuter que le bytecode d'un seul thread. Cela empêche l'utilisation efficace des cœurs CPU multiples dans les tâches intensives en calcul (comme les calculs à grande échelle), car le multitraitement entraîne des surcoûts dus aux changements de contexte du GIL, avec une efficacité parfois inférieure au monolithique.
Le multiprocessus contourne ce problème : chaque processus dispose de son propre interpréteur Python et de son propre GIL, permettant l'exécution simultanée sur plusieurs cœurs CPU pour un calcul véritablement parallèle et une amélioration de l'efficacité des tâches intensives en CPU.
L'outil principal pour créer des processus en Python : le module multiprocessing
Processus zombies et processus orphelins
Contexte
- Dans les systèmes Unix/Linux, normalement tous les processus enfants sont créés par des processus parents, qui à leur tour créent leurs propres processus enfants.
- La terminaison d'un processus enfant et l'exécution de son processus parent sont un processus asynchrone : lorsqu'un processus se termine, son parent n'en est pas informé immédiatement. Un mécanisme est nécessaire pour finaliser le travail après la terminaison, impliquant que le parent doit appeler les fonctions
waitetwaitpidpour obtenir l'état de terminaison de l'enfant. - Si le parent ne réagit pas ou rencontre des problèmes lorsque l'enfant meurt, celui-ci risque d'être négligé.
Processus zombies
Un processus zombie est un processus enfant qui est mort mais dont le parent n'a pas récupéré son état de terminaison. Ces processus continuent d'exister dans la liste des processus actuels et consomment des ressources système, affectant ainsi le fonctionnement global.
Processus orphelins
Un processus orphelin est un processus enfant dont le parent s'est terminé avant lui, interrompant la communication entre parent et enfant. Ces processus sont pris en charge par le processus init (le "père" de tous les processus), qui se charge de libérer leurs ressources.
Impact des processus zombies et orphelins
Les processus zombies sont plus dangereux car ils ne se ferment pas automatiquement et continuent d'occuper des ressources système.
Solutions pour les processus zombies
- Terminer manuellement le processus parent ou enfant.
- Utiliser
join()lors de la création de processus, ce qui récupère automatiquement les processus zombies. - Implémenter un mécanisme de signalisation (explication disponible dans un autre article).
- Faire en sorte que l'enfant envoie un signal au parent une fois terminé, permettant au parent de le supprimer activement.
Processus démons
Un démon est un processus spécial qui démarre au lancement du système et continue de s'exécuter en arrière-plan. Ces processus n'interagissent généralement pas directement avec l'utilisateur et n'acceptent pas d'entrée ou de sortie standard. Ils exécutent des tâches en arrière-plan ou fournissent des services.
Les démons sont souvent démarrés manuellement par l'administrateur système et peuvent être configurés pour démarrer automatiquement au lancement du système, fonctionnant jusqu'à l'arrêt du système ou jusqu'à leur arrêt explicite.
Les démons courants incluent les services réseau (serveurs web, serveurs de messagerie, serveurs FTP), les systèmes d'enregistrement de journaux (service de journaux système, services de journaux d'applications), etc.
Étant donné qu'ils s'exécutent en arrière-plan sans interaction utilisateur et avec des privilèges élevés, la création de démons nécessite une attention particulière à la sécurité et à la stabilité.
import multiprocessing
import time
def tache_demon():
"""Tâche de démon : impression continue des journaux"""
while True:
print("Le démon fonctionne toujours...")
time.sleep(1)
def tache_normale():
"""Tâche de processus normal : se termine après 3 secondes"""
time.sleep(3)
print("Processus normal terminé")
if __name__ == '__main__':
# Créer un démon (daemon=True)
processus_demon = multiprocessing.Process(target=tache_demon)
processus_demon.daemon = True # Marquer comme démon
# Créer un processus normal
processus_normal = multiprocessing.Process(target=tache_normale)
# Démarrer les processus
processus_demon.start()
processus_normal.start()
# Le processus principal attend la fin du processus normal (après 3 secondes)
processus_normal.join()
print("Processus principal terminé")
# Après la fin du processus principal, le démon est automatiquement terminé (ne continue plus d'imprimer)
# Résultat :
# Le démon fonctionne toujours...
# Le démon fonctionne toujours...
# Le démon fonctionne toujours...
# Processus normal terminé
# Processus principal terminé
Dans l'exemple ci-dessus, le démon devait initialement continuer d'imprimer indéfiniment, mais le processus principal se termine après le processus normal, forçant l'arrêt du démon qui cesse d'afficher.
Communication Inter-Processus (IPC)
En informatique, l'IPC (Inter-Process Communication) désigne les mécanismes permettant à différents processus (programmes en cours d'exécution) dans un système d'exploitation d'échanger des données, de transmettre des signaux ou de coordonner leurs actions.
Étant donné que chaque processus possède un espace d'adressage indépendant en mémoire (pour la sécurité et l'isolation), les processus ne peuvent pas accéder directement à la mémoire les uns des autres. Ils doivent donc utiliser des interfaces spécialisées fournies par le système d'exploitation pour communiquer.
Le module multiprocessing de Python (files d'attente, canaux) encapsule divers outils IPC, simplifiant le transfert de données et la coordination entre processus.
Canaux (Pipe) : transmission bidirectionnelle de données
Les canaux sont semi-duplex, les données ne pouvant circuler que dans une seule direction. Pour une communication bidirectionnelle, deux canaux sont nécessaires. Les stdout et stderr de l'exécution de commandes via subprocess.Popen sont des exemples de canaux. Une fois qu'un message est placé dans un canal, il ne peut être lu qu'une seule fois.
Caractéristiques :
- Convient pour la communication entre deux processus (un-à-un) ;
- La transmission de données est bidirectionnelle (plex) ;
- Fondé sur le mécanisme de canal du système d'exploitation, donc rapide ;
- Attention : si deux processus lisent et écrivent simultanément, cela peut entraîner des données désordonnées, nécessitant une synchronisation manuelle (comme l'ajout de verrous).
import multiprocessing
def emetteur(connexion):
# Envoyer des données
connexion.send("Bonjour de l'émetteur")
connexion.send(456)
connexion.close() # Fermer la connexion
def recepteur(connexion):
# Recevoir des données (recv() bloque jusqu'à réception de données)
print(connexion.recv()) # Affiche : Bonjour de l'émetteur
print(connexion.recv()) # Affiche : 456
connexion.close()
if __name__ == '__main__':
# Créer un canal, retourne deux objets de connexion
conn1, conn2 = multiprocessing.Pipe()
# Créer les processus émetteur et récepteur, en passant des objets de connexion différents
p_emetteur = multiprocessing.Process(target=emetteur, args=(conn1,))
p_recepteur = multiprocessing.Process(target=recepteur, args=(conn2,))
p_emetteur.start()
p_recepteur.start()
p_emetteur.join()
p_recepteur.join()
Files d'attente (Queue) : tampon de données sécurisé pour multiprocessus
Une file d'attente est un tampon "premier entré, premier sorti" (FIFO) créé avec multiprocessing.Queue(). Plusieurs processus peuvent y placer des données via put() ou en extraire via get(). Sa mise en œuvre repose sur des canaux et des verrous, garantissant la sécurité entre threads et processus.
Caractéristiques :
- Convient pour la communication entre plusieurs processus (multiproducteurs, multiconsommateurs) ;
- Pas besoin de gérer manuellement la synchronisation (verrous internes déjà gérés) ;
- Prise en charge des opérations bloquantes/non bloquantes (comme
get(block=False)pour un retrait non bloquant, lève une exception si aucune donnée disponible) ; JoinableQueueest une version améliorée de la file d'attente, prenant en chargetask_done()etjoin(), permettant d'attendre la fin du traitement de toutes les tâches de la file.
import multiprocessing
import time
def producteur(file, nom):
"""Producteur : place des données dans la file"""
for i in range(3):
donnees = f"Données de {nom} {i}"
file.put(donnees)
print(f"{nom} produit : {donnees}")
time.sleep(0.5) # Simuler un traitement
def consommateur(file, nom):
"""Consommateur : extrait des données de la file"""
while True:
donnees = file.get() # Attendre des données (bloquant)
if donnees is None: # Signal d'arrêt
break
print(f"{nom} consomme : {donnees}")
time.sleep(1) # Simuler un traitement
file.task_done() # Notifier la file que la tâche est traitée (nécessaire uniquement pour JoinableQueue)
if __name__ == '__main__':
# Créer une file d'attente attendable (appropriée pour les scénarios nécessitant de confirmer l'achèvement des tâches)
file = multiprocessing.JoinableQueue()
# Créer 2 producteurs et 2 consommateurs
producteurs = [
multiprocessing.Process(target=producteur, args=(file, f"Producteur{i}"))
for i in range(2)
]
consommateurs = [
multiprocessing.Process(target=consommateur, args=(file, f"Consommateur{i}"))
for i in range(2)
]
# Démarrer tous les processus
for p in producteurs:
p.start()
for c in consommateurs:
c.start()
# Attendre la fin de tous les producteurs
for p in producteurs:
p.join()
# Envoyer un signal d'arrêt à la file (un par consommateur)
for _ in consommateurs:
file.put(None)
# Attendre la fin de tous les consommateurs
for c in consommateurs:
c.join()
# Attendre la fin de toutes les tâches dans la file
file.join()
print("Toutes les tâches ont été traitées")
Pour plus d'informations sur les signaux, vous pouvez consulter un autre article sur le sujet.