Resumen:
- Durante los últimos 15 años, el módulo
subprocessde Python y la bibliotecapsutilhan usado un enfoque ineficiente de “sondeo busy-loop” al esperar la finalización de procesos (wait()), repitiendosleepywaitpid. - Este enfoque provoca activaciones innecesarias de la CPU, consumo de batería, latencia en la detección de la finalización del proceso y mala escalabilidad al monitorear muchos procesos.
- Con una actualización reciente, en Linux se implementó una verdadera “espera basada en eventos” usando
pidfd_open()ypoll(), y en BSD/macOS usandokqueue(). - Windows ya usaba
WaitForSingleObject, por lo que no hay cambios allí, pero en sistemas POSIX se eliminan los cambios de contexto innecesarios y el uso de CPU converge a “0”.
Resumen detallado:
1. Un problema que duró 15 años: el sondeo busy-loop
Desde que en Python 3.3 se agregó el parámetro timeout a subprocess.Popen.wait(), la biblioteca estándar de Python y la muy utilizada biblioteca psutil han estado usando un método ineficiente para esperar la finalización de procesos.
La lógica anterior era simple, pero ineficiente:
- Verificar el estado del proceso con
waitpid(WNOHANG)(no bloqueante) - Si no terminó, hacer
sleep()un momento (con exponential backoff) - Volver al paso 1 y repetir
# Método anterior (código conceptual)
import time, os
def wait_busy(pid, timeout):
delay = 0.0001
while True:
# Verificar si el proceso terminó (polling)
if os.waitpid(pid, os.WNOHANG) == (pid, status):
return status
time.sleep(delay)
delay = min(delay * 2, 0.040) # aumentar el tiempo de espera hasta un máximo de 40 ms
Este enfoque tiene tres desventajas críticas.
- Activaciones de CPU: por más que aumente el tiempo de espera, el sistema tiene que despertarse periódicamente para revisar el estado, desperdiciando ciclos de CPU y consumiendo energía.
- Latencia: inevitablemente hay una diferencia de tiempo entre el momento real en que termina el proceso y el momento en que se detecta al despertar de
sleep. - Escalabilidad: en entornos de servidor donde hay que monitorear cientos o miles de procesos al mismo tiempo, este overhead crece rápidamente.
2. La solución: espera basada en eventos para sistemas POSIX
Todos los sistemas POSIX ofrecen mecanismos para detectar cambios de estado en descriptores de archivo (select, poll, epoll, kqueue). Recientemente, Python y psutil mejoraron su implementación para aprovechar esto en la detección de PID de procesos.
- Linux: usa la syscall
pidfd_open(), introducida en el kernel Linux 5.3 en 2019. Esta devuelve un descriptor de archivo que apunta al PID del proceso, el cual puede registrarse enpoll()oepoll()para vigilar el evento de finalización del proceso. (Se agregó al móduloosdesde Python 3.9). - BSD / macOS: usan el filtro
EVFILT_PROCde la syscallkqueue()para monitorear eficientemente eventos de procesos. - Windows: ya soportaba espera basada en eventos mediante la API
WaitForSingleObject, por lo que no hay cambios.
3. Mejora de rendimiento y resultados
Con este cambio, cuando se llama a wait(), el proceso pasa al estado de “interruptible sleep” desde la perspectiva del kernel. Es decir, espera silenciosamente en espacio de kernel sin consumir absolutamente nada de CPU, y se despierta de inmediato cuando llega la señal de finalización del proceso.
Según benchmarks realizados con /usr/bin/time -v y otras herramientas, en comparación con el método anterior se redujeron drásticamente los cambios de contexto innecesarios, y también mejoró de forma inmediata la velocidad de detección de finalización del proceso. Esta actualización ya se incorporó a la biblioteca psutil y al core de CPython, por lo que los desarrolladores de Python podrán beneficiarse de esta mejora de rendimiento en el futuro sin tener que modificar su código.
Aún no hay comentarios.