Résumé :
- Le module
subprocessde Python et la bibliothèquepsutilutilisent depuis 15 ans une méthode inefficace de « polling en busy loop » lors de l’attente de la fin d’un processus (wait()), en répétantsleepetwaitpid. - Cette approche entraîne des réveils CPU inutiles, une consommation de batterie accrue, une latence dans la détection de la fin des processus, et passe mal à l’échelle lorsqu’il faut surveiller de nombreux processus.
- Une mise à jour récente introduit enfin une vraie attente événementielle sur Linux via
pidfd_open()etpoll(), et sur BSD/macOS viakqueue(). - Windows utilisait déjà
WaitForSingleObject, donc rien ne change de ce côté-là, mais sur les systèmes POSIX, les changements de contexte inutiles disparaissent et l’usage CPU en attente converge vers0.
Résumé détaillé :
1. Un problème qui a duré 15 ans : le polling en busy loop
Depuis l’ajout du paramètre timeout à subprocess.Popen.wait() dans Python 3.3, la bibliothèque standard de Python ainsi que la très utilisée bibliothèque psutil reposent sur une méthode inefficace pour attendre la fin d’un processus.
L’ancienne logique était simple, mais peu efficace :
- Vérifier l’état du processus avec
waitpid(WNOHANG)(non bloquant) - S’il n’est pas terminé, faire un court
sleep()(avec exponential backoff) - Revenir à l’étape 1 et répéter
# Ancienne approche (code conceptuel)
import time, os
def wait_busy(pid, timeout):
delay = 0.0001
while True:
# Vérifie si le processus est terminé (polling)
if os.waitpid(pid, os.WNOHANG) == (pid, status):
return status
time.sleep(delay)
delay = min(delay * 2, 0.040) # Augmente l’attente jusqu’à 40 ms max
Cette méthode présente trois défauts majeurs.
- Réveils CPU : même en augmentant le délai d’attente, le système doit se réveiller périodiquement pour vérifier l’état, ce qui gaspille des cycles CPU et de l’énergie.
- Latence : il existe inévitablement un décalage entre le moment réel où le processus se termine et celui où cette fin est détectée après le réveil du
sleep. - Passage à l’échelle : dans les environnements serveur où il faut surveiller simultanément des centaines ou des milliers de processus, ce surcoût augmente rapidement.
2. La solution : une attente événementielle pour les systèmes POSIX
Tous les systèmes POSIX fournissent des mécanismes de détection des changements d’état des descripteurs de fichiers (select, poll, epoll, kqueue). Python et psutil ont récemment été améliorés pour réutiliser ces mécanismes afin de détecter l’état des PID.
- Linux : utilisation de l’appel système
pidfd_open(), introduit dans le noyau Linux 5.3 en 2019. Il renvoie un descripteur de fichier pointant vers un PID, qui peut ensuite être enregistré danspoll()ouepoll()pour surveiller l’événement de fin du processus. (Ajouté au moduleosà partir de Python 3.9) - BSD / macOS : utilisation du filtre
EVFILT_PROCde l’appel systèmekqueue()pour surveiller efficacement les événements liés aux processus. - Windows : l’attente événementielle était déjà prise en charge via l’API
WaitForSingleObject, donc aucun changement ici.
3. Gains de performance et résultats
Avec ce changement, lors d’un appel à wait(), le processus passe du point de vue du noyau dans un état d’« interruptible sleep ». Autrement dit, il attend silencieusement dans l’espace noyau sans consommer du tout de CPU, puis se réveille immédiatement quand le signal de fin du processus survient.
D’après les benchmarks réalisés notamment avec /usr/bin/time -v, les changements de contexte inutiles ont fortement diminué par rapport à l’ancienne méthode, et la vitesse de détection de fin de processus s’est elle aussi améliorée de façon immédiate. Cette mise à jour a été intégrée à la bibliothèque psutil et au cœur de CPython, ce qui permettra aux développeurs Python d’en profiter sans modification particulière de leur code.
Aucun commentaire pour le moment.