Tiempos de Espera de Hilos y Hilos Demonios
En este paso final, aprenderá sobre dos conceptos importantes en la gestión de hilos: establecer tiempos de espera y usar hilos demonios. Estas técnicas le dan más control sobre cómo se comportan los hilos e interactúan con el programa principal.
Trabajando con Tiempos de Espera de Hilos
Como aprendió en el Paso 2, el método join() acepta un parámetro de tiempo de espera (timeout). Esto es útil cuando desea esperar a que un hilo se complete, pero solo hasta un cierto punto.
Creemos un ejemplo más práctico donde implementamos una función que intenta obtener datos con un tiempo de espera:
-
Cree un nuevo archivo llamado thread_with_timeout.py en el directorio /home/labex/project.
-
Agregue el siguiente código:
import threading
import time
import random
def fetch_data(data_id):
"""Simula la obtención de datos que podrían tardar cantidades variables de tiempo."""
print(f"Obteniendo datos #{data_id}...")
## Simula diferentes tiempos de obtención, ocasionalmente muy largos
fetch_time = random.choices([1, 8], weights=[0.8, 0.2])[0]
time.sleep(fetch_time)
if fetch_time > 5: ## Simula una obtención lenta
print(f"Datos #{data_id}: ¡La obtención tardó demasiado!")
return None
else:
print(f"Datos #{data_id}: Obtención completada en {fetch_time} segundos!")
return f"Contenido de datos para #{data_id}"
def fetch_with_timeout(data_id, timeout=3):
"""Obtiene datos con un tiempo de espera."""
result = [None] ## Usando una lista para almacenar el resultado del hilo
def target_func():
result[0] = fetch_data(data_id)
## Crea e inicia el hilo
thread = threading.Thread(target=target_func)
thread.start()
## Espera al hilo con un tiempo de espera
thread.join(timeout=timeout)
if thread.is_alive():
print(f"Datos #{data_id}: ¡La obtención expiró después de {timeout} segundos!")
return "TIMEOUT"
else:
return result[0]
## Intenta obtener varios datos
for i in range(1, 6):
print(f"\nIntentando obtener datos #{i}")
result = fetch_with_timeout(i, timeout=3)
if result == "TIMEOUT":
print(f"Hilo principal: La obtención de datos #{i} expiró, continuando...")
elif result is None:
print(f"Hilo principal: La obtención de datos #{i} se completó pero no devolvió datos.")
else:
print(f"Hilo principal: Obtenido con éxito: {result}")
print("\n¡Todos los intentos de obtención se completaron!")
- Guarde el archivo y ejecútelo:
python3 /home/labex/project/thread_with_timeout.py
La salida variará, pero debería ser similar a esto:
Intentando obtener datos #1
Obteniendo datos #1...
Datos #1: Obtención completada en 1 segundos!
Hilo principal: Obtenido con éxito: Contenido de datos para #1
Intentando obtener datos #2
Obteniendo datos #2...
Datos #2: Obtención completada en 1 segundos!
Hilo principal: Obtenido con éxito: Contenido de datos para #2
Intentando obtener datos #3
Obteniendo datos #3...
Datos #3: La obtención expiró después de 3 segundos!
Hilo principal: La obtención de datos #3 expiró, continuando...
Datos #3: ¡La obtención tardó demasiado!
Intentando obtener datos #4
Obteniendo datos #4...
Datos #4: Obtención completada en 1 segundos!
Hilo principal: Obtenido con éxito: Contenido de datos para #4
Intentando obtener datos #5
Obteniendo datos #5...
Datos #5: Obtención completada en 1 segundos!
Hilo principal: Obtenido con éxito: Contenido de datos para #5
¡Todos los intentos de obtención se completaron!
Este ejemplo demuestra:
- Una función que intenta obtener datos y podría ser lenta
- Una función envolvente que usa hilos con un tiempo de espera
- Cómo manejar los tiempos de espera con elegancia y continuar con otras operaciones
Entendiendo los Hilos Demonios
En Python, los hilos demonios son hilos que se ejecutan en segundo plano. La diferencia clave entre los hilos demonios y los no demonios es que Python no esperará a que los hilos demonios se completen antes de salir. Esto es útil para los hilos que realizan tareas en segundo plano que no deberían impedir que el programa salga.
Creemos un ejemplo para demostrar los hilos demonios:
-
Cree un nuevo archivo llamado daemon_threads.py en el directorio /home/labex/project.
-
Agregue el siguiente código:
import threading
import time
def background_task(name, interval):
"""Una tarea que se ejecuta en segundo plano a intervalos regulares."""
count = 0
while True:
count += 1
print(f"{name}: Iteración {count} a las {time.strftime('%H:%M:%S')}")
time.sleep(interval)
def main_task():
"""La tarea principal que se ejecuta durante un período de tiempo establecido."""
print("Tarea principal: Iniciando...")
time.sleep(5)
print("Tarea principal: ¡Completada!")
## Crea dos hilos en segundo plano
print("Creando hilos de monitoreo en segundo plano...")
monitor1 = threading.Thread(target=background_task, args=("Monitor-1", 1), daemon=True)
monitor2 = threading.Thread(target=background_task, args=("Monitor-2", 2), daemon=True)
## Inicia los hilos en segundo plano
monitor1.start()
monitor2.start()
print("Monitores en segundo plano iniciados, ahora iniciando la tarea principal...")
## Ejecuta la tarea principal
main_task()
print("Tarea principal completada, el programa saldrá sin esperar a los hilos demonios.")
print("Los hilos demonios se terminarán cuando el programa salga.")
- Guarde el archivo y ejecútelo:
python3 /home/labex/project/daemon_threads.py
La salida debería ser similar a esto:
Creando hilos de monitoreo en segundo plano...
Monitores en segundo plano iniciados, ahora iniciando la tarea principal...
Tarea principal: Iniciando...
Monitor-1: Iteración 1 a las 14:25:10
Monitor-2: Iteración 1 a las 14:25:10
Monitor-1: Iteración 2 a las 14:25:11
Monitor-1: Iteración 3 a las 14:25:12
Monitor-2: Iteración 2 a las 14:25:12
Monitor-1: Iteración 4 a las 14:25:13
Monitor-1: Iteración 5 a las 14:25:14
Monitor-2: Iteración 3 a las 14:25:14
Tarea principal: ¡Completada!
Tarea principal completada, el programa saldrá sin esperar a los hilos demonios.
Los hilos demonios se terminarán cuando el programa salga.
En este ejemplo:
- Creamos dos hilos demonios que se ejecutan continuamente, imprimiendo mensajes a intervalos regulares.
- Establecimos
daemon=True al crear los hilos, lo que los marca como hilos demonios.
- El hilo principal se ejecuta durante 5 segundos y luego sale.
- Cuando el hilo principal sale, el programa termina y los hilos demonios también se terminan automáticamente.
Hilos No Demonios vs. Hilos Demonios
Para entender mejor la diferencia, creemos un ejemplo más que compare hilos demonios y no demonios:
-
Cree un nuevo archivo llamado daemon_comparison.py en el directorio /home/labex/project.
-
Agregue el siguiente código:
import threading
import time
def task(name, seconds, daemon=False):
"""Una tarea que se ejecuta durante una cantidad de tiempo especificada."""
print(f"{name} iniciando {'(demonio)' if daemon else '(no-demonio)'}")
time.sleep(seconds)
print(f"{name} finalizado después de {seconds} segundos")
## Crea un hilo no demonio que se ejecuta durante 8 segundos
non_daemon_thread = threading.Thread(
target=task,
args=("Hilo no-demonio", 8, False),
daemon=False ## Este es el valor predeterminado, por lo que en realidad no es necesario
)
## Crea un hilo demonio que se ejecuta durante 8 segundos
daemon_thread = threading.Thread(
target=task,
args=("Hilo demonio", 8, True),
daemon=True
)
## Inicia ambos hilos
non_daemon_thread.start()
daemon_thread.start()
## Deja que el hilo principal se ejecute durante 3 segundos
print("El hilo principal se ejecutará durante 3 segundos...")
time.sleep(3)
## Comprueba qué hilos todavía se están ejecutando
print("\nDespués de 3 segundos:")
print(f"El hilo demonio está vivo: {daemon_thread.is_alive()}")
print(f"El hilo no-demonio está vivo: {non_daemon_thread.is_alive()}")
print("\nEl hilo principal está finalizando. Esto es lo que sucederá:")
print("1. El programa esperará a que todos los hilos no demonios se completen")
print("2. Los hilos demonios se terminarán cuando el programa salga")
print("\nEsperando a que los hilos no demonios finalicen...")
## No necesitamos unir el hilo no demonio, Python esperará por él
## Pero lo uniremos explícitamente para mayor claridad
non_daemon_thread.join()
print("Todos los hilos no demonios han finalizado, el programa saldrá ahora.")
- Guarde el archivo y ejecútelo:
python3 /home/labex/project/daemon_comparison.py
La salida debería ser similar a esto:
Hilo no-demonio iniciando (no-demonio)
Hilo demonio iniciando (demonio)
El hilo principal se ejecutará durante 3 segundos...
Después de 3 segundos:
El hilo demonio está vivo: True
El hilo no-demonio está vivo: True
El hilo principal está finalizando. Esto es lo que sucederá:
1. El programa esperará a que todos los hilos no demonios se completen
2. Los hilos demonios se terminarán cuando el programa salga
Esperando a que los hilos no demonios finalicen...
Hilo no-demonio finalizado después de 8 segundos
Todos los hilos no demonios han finalizado, el programa saldrá ahora.
Observaciones clave:
- Ambos hilos se inician y se ejecutan concurrentemente.
- Después de 3 segundos, ambos hilos todavía se están ejecutando.
- El programa espera a que el hilo no demonio finalice (después de 8 segundos).
- El hilo demonio todavía se está ejecutando cuando el programa sale, pero se termina.
- El hilo demonio nunca llega a imprimir su mensaje de finalización porque se termina cuando el programa sale.
Cuándo Usar Hilos Demonios
Los hilos demonios son útiles para:
- Tareas de monitoreo en segundo plano
- Operaciones de limpieza
- Servicios que deben ejecutarse durante la duración del programa pero no impedir que salga
- Hilos de temporizador que activan eventos a intervalos regulares
Los hilos no demonios son apropiados para:
- Operaciones críticas que deben completarse
- Tareas que no deben ser interrumpidas
- Operaciones que deben finalizar limpiamente antes de que el programa salga
Comprender cuándo usar cada tipo es una parte importante del diseño de aplicaciones robustas con múltiples hilos.