Que tus "futures" de larga duración no impidan que tu programa Python termine

En Python: Olvídate de los hilos y usa "futures" defiendo el uso de Futures en vez de hilos tradicionales por su mayor facilidad de uso, pero vemos que no existe una forma simple de marcar un Future como Daemon. Es decir, que el programa no terminará su ejecución hasta que todos los Futures se completen.

Esto no está mal. De hecho nos asegura que los recursos utilizados por los Futures se liberen de forma ordenada. Puede ser necesario borrar ficheros temporales o desconectarse de la base de datos de forma adecuada, por ejemplo. [1]

Con el modelo de actores que empleo habitualmente, los Futures no terminan nunca. Se pueden quedar ahí esperando eternamente a que alguien les pida algo. Por supuesto es posible definir un mensaje global que solicite un suicidio colectivo, pero ello implica la gestión de un mensaje adicional y el registro de todos los actores en algún sitio. Ninguna de las dos cosas es realmente un problema, pero en muchos casos no necesitamos estas complejidades.

Un ejemplo común es el caso de Futures de larga duración esperando trabajo mientras el hilo principal supervisa que no falle ninguno. Si alguno muere inesperadamente, el hilo principal lo detecta, notifica la excepción y el programa termina.

Existen dos posibilidades básicas, a priori:

  1. Marcar los hilos subyacentes a los Futures como Daemons para que mueran cuando el hilo principal termine.

    Nota

    En realidad este enfoque no funciona porque los hilos y procesos que ejecutan los Futures sí que están definidos como Daemon.

    ¿Qué está ocurriendo entonces? Mira el punto siguiente.

  2. Estudiando el código fuente de la librería concurrent.futures podemos ver que registra un manejador en atexit. Esto indica a Python que ese manejador debe invocarse cuando el programa está a punto de salir. Ese manejador gestiona, precisamente, la espera por los Futures en curso. No vuelve hasta que todos ellos han terminado.

    Dado esto, lo obvio es eliminar ese registro:

    import concurrent.futures
    import atexit
    atexit.unregister(concurrent.futures.thread._python_exit)
    atexit.unregister(concurrent.futures.process._python_exit)
    

    Si solo usas hilos o multiprocesos, solo necesitas eliminar el registro correspondiente.

    Con este cambio el programa terminará cuando el hilo principal acabe aunque haya Futures en curso. [2]

Por supuesto, nada de esto es un problema si usamos los Futures para realizar trabajos cortos que se completan enseguida. Dado que los hilos subyacentes no se crean ni destruyen sino que constituyen un pool que va recibiendo el trabajo a través de una cola, es un uso más simple y eficiente que usar hilos directamente.

[1] Dado que el programa puede fallar en cualquier momento por un corte de luz, un cuelgue del sistema operativo, etc., depender del cierre ordenado de recursos es poco inteligente. El entorno debe sobrevivir ante estos eventos, aunque aproveche el cierre ordenado de recursos en el 99.999% de los casos para optimizar su funcionamiento. No obstante es crítico recordar que no podemos contar con ello al 100%.
[2] Si hay Futures realizando trabajo durante el cierre del programa y no permitimos que terminen, es frecuente que ocurran errores espurios. Esto es debido a que el hilo correspondiente sigue funcionando mientras el intérprete Python se está destruyendo. Esto no es un problema en mi entorno, pero si no lo has visto antes y no entiendes el motivo te llevarás un buen susto.