¿Cómo esperar a que un hilo de Python finalice?

PythonBeginner
Practicar Ahora

Introducción

Dominar cómo esperar a que los hilos (threads) de Python finalicen es esencial para construir aplicaciones robustas y confiables. En programas multi-hilo, la sincronización adecuada asegura que las operaciones se completen en el orden correcto y que los recursos se utilicen eficientemente.

En este laboratorio, aprenderá a crear hilos de Python, a esperar a que se completen y a manejar múltiples hilos. Estas habilidades son fundamentales para desarrollar aplicaciones concurrentes que pueden realizar múltiples tareas simultáneamente, manteniendo una sincronización adecuada.

Creando tu Primer Hilo de Python

El módulo threading de Python proporciona una forma sencilla de crear y gestionar hilos (threads). En este paso, aprenderá a crear un hilo básico y a observar su comportamiento.

Entendiendo los Hilos (Threads)

Un hilo (thread) es un flujo de ejecución separado en un programa. Cuando ejecuta un script de Python, comienza con un único hilo llamado el hilo principal (main thread). Al crear hilos adicionales, su programa puede realizar múltiples tareas concurrentemente.

Los hilos son útiles para:

  • Ejecutar operaciones que consumen mucho tiempo sin bloquear el programa principal
  • Procesar tareas en paralelo para mejorar el rendimiento
  • Manejar múltiples conexiones de cliente en una aplicación de servidor

Creando un Hilo Simple

Comencemos creando un script de Python simple que demuestre cómo crear e iniciar un hilo.

  1. Abra un nuevo archivo en el editor haciendo clic en el menú "Archivo", seleccionando "Nuevo archivo" y luego guardándolo como simple_thread.py en el directorio /home/labex/project.

  2. Agregue el siguiente código al archivo:

import threading
import time

def print_numbers():
    """Función que imprime números del 1 al 5 con un retraso."""
    for i in range(1, 6):
        print(f"Número {i} del hilo")
        time.sleep(1)  ## Espera 1 segundo

## Crea un hilo que apunta a la función print_numbers
number_thread = threading.Thread(target=print_numbers)

## Inicia el hilo
print("Iniciando el hilo...")
number_thread.start()

## El hilo principal continúa la ejecución
print("El hilo principal continúa ejecutándose...")
print("El hilo principal está haciendo otro trabajo...")

## Espera 2 segundos para demostrar que ambos hilos se ejecutan concurrentemente
time.sleep(2)
print("¡El hilo principal terminó su trabajo!")
  1. Guarde el archivo presionando Ctrl+S o haciendo clic en "Archivo" > "Guardar".

  2. Ejecute el script abriendo una terminal (si aún no está abierta) y ejecutando:

python3 /home/labex/project/simple_thread.py

Debería ver una salida similar a esta:

Iniciando el hilo...
El hilo principal continúa ejecutándose...
El hilo principal está haciendo otro trabajo...
Número 1 del hilo
Número 2 del hilo
¡El hilo principal terminó su trabajo!
Número 3 del hilo
Número 4 del hilo
Número 5 del hilo

Analizando lo que Sucedió

En este ejemplo:

  1. Importamos los módulos threading y time.
  2. Definimos una función print_numbers() que imprime números del 1 al 5 con un retraso de 1 segundo entre cada uno.
  3. Creamos un objeto de hilo, especificando la función a ejecutar usando el parámetro target.
  4. Iniciamos el hilo usando el método start().
  5. El hilo principal continuó su ejecución, imprimiendo mensajes y esperando 2 segundos.
  6. Tanto el hilo principal como nuestro hilo de números se ejecutaron concurrentemente, por lo que la salida está intercalada.

Observe que el hilo principal finalizó antes de que el hilo de números imprimiera todos sus números. Esto se debe a que los hilos se ejecutan independientemente y, de forma predeterminada, el programa de Python saldrá cuando el hilo principal finalice, incluso si otros hilos aún se están ejecutando.

En el siguiente paso, aprenderá a esperar a que un hilo se complete usando el método join().

Esperando a que un Hilo se Complete con join()

En el paso anterior, creó un hilo que se ejecutaba independientemente del hilo principal. Sin embargo, hay muchas situaciones en las que necesita esperar a que un hilo termine su trabajo antes de continuar con el resto de su programa. Aquí es donde el método join() resulta útil.

Entendiendo el Método join()

El método join() de un objeto de hilo bloquea el hilo que lo llama (generalmente el hilo principal) hasta que el hilo cuyo método join() se llama finaliza. Esto es esencial cuando:

  • El hilo principal necesita resultados de un hilo de trabajo
  • Necesita asegurarse de que todos los hilos se completen antes de salir del programa
  • El orden de las operaciones es importante para la lógica de su aplicación

Creando un Hilo y Esperando a que se Complete

Modifiquemos nuestro ejemplo anterior para demostrar cómo esperar a que un hilo se complete usando el método join().

  1. Cree un nuevo archivo llamado join_thread.py en el directorio /home/labex/project.

  2. Agregue el siguiente código al archivo:

import threading
import time

def calculate_sum(numbers):
    """Función que calcula la suma de números con un retraso."""
    print("Iniciando el cálculo...")
    time.sleep(3)  ## Simula un cálculo que consume tiempo
    result = sum(numbers)
    print(f"Resultado del cálculo: {result}")
    return result

## Crea una lista de números
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

## Crea un hilo que apunta a la función calculate_sum
calculation_thread = threading.Thread(target=calculate_sum, args=(numbers,))

## Inicia el hilo
print("Hilo principal: Iniciando el hilo de cálculo...")
calculation_thread.start()

## Realiza algún otro trabajo en el hilo principal
print("Hilo principal: Haciendo otro trabajo mientras espera...")
for i in range(5):
    print(f"Hilo principal: Trabajando... ({i+1}/5)")
    time.sleep(0.5)

## Espera a que el hilo de cálculo se complete
print("Hilo principal: Esperando a que el hilo de cálculo finalice...")
calculation_thread.join()
print("Hilo principal: ¡El hilo de cálculo ha finalizado!")

## Continúa con el hilo principal
print("Hilo principal: Continuando con el resto del programa...")
  1. Guarde el archivo y ejecútelo con el siguiente comando:
python3 /home/labex/project/join_thread.py

Debería ver una salida similar a esta:

Hilo principal: Iniciando el hilo de cálculo...
Iniciando el cálculo...
Hilo principal: Haciendo otro trabajo mientras espera...
Hilo principal: Trabajando... (1/5)
Hilo principal: Trabajando... (2/5)
Hilo principal: Trabajando... (3/5)
Hilo principal: Trabajando... (4/5)
Hilo principal: Trabajando... (5/5)
Hilo principal: Esperando a que el hilo de cálculo finalice...
Resultado del cálculo: 55
Hilo principal: ¡El hilo de cálculo ha finalizado!
Hilo principal: Continuando con el resto del programa...

La Importancia de join()

En este ejemplo:

  1. Creamos un hilo que realiza un cálculo (sumando números).
  2. El hilo principal hizo algún otro trabajo concurrentemente.
  3. Cuando el hilo principal necesitaba asegurarse de que el cálculo se completara, llamó a calculation_thread.join().
  4. El método join() hizo que el hilo principal esperara hasta que el hilo de cálculo finalizara.
  5. Después de que el hilo de cálculo se completó, el hilo principal continuó su ejecución.

Este patrón es muy útil cuando necesita asegurarse de que todas las tareas en hilos se completen antes de continuar con el resto de su programa. Sin join(), el hilo principal podría continuar e incluso salir antes de que los hilos de trabajo hayan completado sus tareas.

Usando join() con un Timeout

A veces, es posible que desee esperar a un hilo, pero no indefinidamente. El método join() acepta un parámetro de tiempo de espera (timeout) opcional que especifica el número máximo de segundos a esperar.

Modifiquemos nuestro código para demostrar esto:

  1. Cree un nuevo archivo llamado join_timeout.py en el directorio /home/labex/project.

  2. Agregue el siguiente código:

import threading
import time

def long_running_task():
    """Una función que simula una tarea de muy larga duración."""
    print("Tarea de larga duración iniciada...")
    time.sleep(10)  ## Simula una tarea de 10 segundos
    print("Tarea de larga duración completada!")

## Crea e inicia el hilo
task_thread = threading.Thread(target=long_running_task)
task_thread.start()

## Espera a que el hilo se complete, pero solo hasta 3 segundos
print("Hilo principal: Esperando hasta 3 segundos...")
task_thread.join(timeout=3)

## Comprueba si el hilo todavía se está ejecutando
if task_thread.is_alive():
    print("Hilo principal: ¡La tarea todavía se está ejecutando, pero de todas formas continúo!")
else:
    print("Hilo principal: La tarea se ha completado dentro del período de tiempo de espera.")

## Continúa con el hilo principal
print("Hilo principal: Continuando con otras operaciones...")
## Esperemos un poco para ver la tarea de larga duración completarse
time.sleep(8)
print("Hilo principal: Finalizado.")
  1. Guarde el archivo y ejecútelo:
python3 /home/labex/project/join_timeout.py

La salida debería verse así:

Tarea de larga duración iniciada...
Hilo principal: Esperando hasta 3 segundos...
Hilo principal: ¡La tarea todavía se está ejecutando, pero de todas formas continúo!
Hilo principal: Continuando con otras operaciones...
Tarea de larga duración completada!
Hilo principal: Finalizado.

En este ejemplo, el hilo principal espera hasta 3 segundos a que el hilo de tarea se complete. Dado que la tarea tarda 10 segundos, el hilo principal continúa después del tiempo de espera, mientras que el hilo de tarea sigue ejecutándose en segundo plano.

Este enfoque es útil cuando desea dar a los hilos la oportunidad de completarse, pero necesita continuar independientemente después de una cierta cantidad de tiempo.

Trabajando con Múltiples Hilos

En aplicaciones del mundo real, a menudo necesita trabajar con múltiples hilos simultáneamente. Este paso le enseñará cómo crear, gestionar y sincronizar múltiples hilos en Python.

Creando Múltiples Hilos

Al tratar con múltiples tareas similares, es común crear múltiples hilos para procesarlas concurrentemente. Esto puede mejorar significativamente el rendimiento, especialmente para operaciones vinculadas a E/S (I/O-bound operations) como la descarga de archivos o la realización de solicitudes de red.

Creemos un ejemplo que utiliza múltiples hilos para procesar una lista de tareas:

  1. Cree un nuevo archivo llamado multiple_threads.py en el directorio /home/labex/project.

  2. Agregue el siguiente código:

import threading
import time
import random

def process_task(task_id):
    """Función para procesar una sola tarea."""
    print(f"Iniciando tarea {task_id}...")
    ## Simula tiempo de procesamiento variable
    processing_time = random.uniform(1, 3)
    time.sleep(processing_time)
    print(f"Tarea {task_id} completada en {processing_time:.2f} segundos.")
    return task_id

## Lista de tareas a procesar
tasks = list(range(1, 6))  ## Tareas con IDs del 1 al 5

## Crea una lista para almacenar nuestros hilos
threads = []

## Crea e inicia un hilo para cada tarea
for task_id in tasks:
    thread = threading.Thread(target=process_task, args=(task_id,))
    threads.append(thread)
    print(f"Hilo creado para la tarea {task_id}")
    thread.start()

print(f"Todos los {len(threads)} hilos han sido iniciados")

## Espera a que todos los hilos se completen
for thread in threads:
    thread.join()

print("¡Todas las tareas se han completado!")
  1. Guarde el archivo y ejecútelo:
python3 /home/labex/project/multiple_threads.py

La salida variará cada vez debido a los tiempos de procesamiento aleatorios, pero debería ser similar a esto:

Hilo creado para la tarea 1
Iniciando tarea 1...
Hilo creado para la tarea 2
Iniciando tarea 2...
Hilo creado para la tarea 3
Iniciando tarea 3...
Hilo creado para la tarea 4
Iniciando tarea 4...
Hilo creado para la tarea 5
Iniciando tarea 5...
Todos los 5 hilos han sido iniciados
Tarea 1 completada en 1.23 segundos.
Tarea 3 completada en 1.45 segundos.
Tarea 2 completada en 1.97 segundos.
Tarea 5 completada en 1.35 segundos.
Tarea 4 completada en 2.12 segundos.
¡Todas las tareas se han completado!

Entendiendo el Flujo de Ejecución

En este ejemplo:

  1. Definimos una función process_task() que simula el procesamiento de una tarea con una duración aleatoria.
  2. Creamos una lista de IDs de tareas (1 a 5).
  3. Para cada tarea, creamos un hilo, lo almacenamos en una lista y lo iniciamos.
  4. Después de iniciar todos los hilos, usamos un segundo bucle con join() para esperar a que cada hilo se complete.
  5. Solo después de que todos los hilos se completaron, imprimimos el mensaje final.

Este patrón es muy útil cuando tiene un lote de tareas independientes que se pueden procesar en paralelo.

Ejecutores de Grupo de Hilos (Thread Pool Executors)

Para una gestión de hilos más avanzada, el módulo concurrent.futures de Python proporciona la clase ThreadPoolExecutor. Esto crea un grupo de hilos de trabajo que se pueden reutilizar, lo que es más eficiente que crear y destruir hilos para cada tarea.

Reescribamos nuestro ejemplo usando un grupo de hilos:

  1. Cree un nuevo archivo llamado thread_pool.py en el directorio /home/labex/project.

  2. Agregue el siguiente código:

import concurrent.futures
import time
import random

def process_task(task_id):
    """Función para procesar una sola tarea."""
    print(f"Iniciando tarea {task_id}...")
    ## Simula tiempo de procesamiento variable
    processing_time = random.uniform(1, 3)
    time.sleep(processing_time)
    print(f"Tarea {task_id} completada en {processing_time:.2f} segundos.")
    return f"Resultado de la tarea {task_id}"

## Lista de tareas a procesar
tasks = list(range(1, 6))  ## Tareas con IDs del 1 al 5

## Crea un ThreadPoolExecutor
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
    ## Envía todas las tareas y almacena los objetos Future
    print(f"Enviando {len(tasks)} tareas al grupo de hilos con 3 trabajadores...")
    future_to_task = {executor.submit(process_task, task_id): task_id for task_id in tasks}

    ## A medida que cada tarea se completa, obtiene su resultado
    for future in concurrent.futures.as_completed(future_to_task):
        task_id = future_to_task[future]
        try:
            result = future.result()
            print(f"Obtenido resultado de la tarea {task_id}: {result}")
        except Exception as e:
            print(f"La tarea {task_id} generó una excepción: {e}")

print("¡Todas las tareas han sido procesadas!")
  1. Guarde el archivo y ejecútelo:
python3 /home/labex/project/thread_pool.py

La salida variará nuevamente debido a los tiempos de procesamiento aleatorios, pero debería ser similar a esto:

Enviando 5 tareas al grupo de hilos con 3 trabajadores...
Iniciando tarea 1...
Iniciando tarea 2...
Iniciando tarea 3...
Tarea 2 completada en 1.15 segundos.
Iniciando tarea 4...
Obtenido resultado de la tarea 2: Resultado de la tarea 2
Tarea 1 completada en 1.82 segundos.
Iniciando tarea 5...
Obtenido resultado de la tarea 1: Resultado de la tarea 1
Tarea 3 completada en 2.25 segundos.
Obtenido resultado de la tarea 3: Resultado de la tarea 3
Tarea 4 completada en 1.45 segundos.
Obtenido resultado de la tarea 4: Resultado de la tarea 4
Tarea 5 completada en 1.67 segundos.
Obtenido resultado de la tarea 5: Resultado de la tarea 5
¡Todas las tareas han sido procesadas!

Beneficios de los Grupos de Hilos

El enfoque del grupo de hilos ofrece varias ventajas:

  1. Gestión de Recursos: Limita el número de hilos que pueden ejecutarse simultáneamente, evitando el agotamiento de los recursos del sistema.
  2. Programación de Tareas: Maneja la programación de tareas automáticamente, iniciando nuevas tareas a medida que los hilos están disponibles.
  3. Recopilación de Resultados: Proporciona formas convenientes de recopilar resultados de tareas completadas.
  4. Manejo de Excepciones: Hace que el manejo de excepciones en los hilos sea más sencillo.

En nuestro ejemplo, establecimos max_workers=3, lo que significa que solo 3 hilos se ejecutarán a la vez, aunque tengamos 5 tareas. A medida que los hilos completan sus tareas, se reutilizan para las tareas restantes.

Los grupos de hilos son particularmente útiles cuando tiene muchas más tareas de las que desea que los hilos se ejecuten simultáneamente, o cuando las tareas se generan continuamente.

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:

  1. Cree un nuevo archivo llamado thread_with_timeout.py en el directorio /home/labex/project.

  2. 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!")
  1. 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:

  1. Una función que intenta obtener datos y podría ser lenta
  2. Una función envolvente que usa hilos con un tiempo de espera
  3. 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:

  1. Cree un nuevo archivo llamado daemon_threads.py en el directorio /home/labex/project.

  2. 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.")
  1. 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:

  1. Creamos dos hilos demonios que se ejecutan continuamente, imprimiendo mensajes a intervalos regulares.
  2. Establecimos daemon=True al crear los hilos, lo que los marca como hilos demonios.
  3. El hilo principal se ejecuta durante 5 segundos y luego sale.
  4. 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:

  1. Cree un nuevo archivo llamado daemon_comparison.py en el directorio /home/labex/project.

  2. 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.")
  1. 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:

  1. Ambos hilos se inician y se ejecutan concurrentemente.
  2. Después de 3 segundos, ambos hilos todavía se están ejecutando.
  3. El programa espera a que el hilo no demonio finalice (después de 8 segundos).
  4. El hilo demonio todavía se está ejecutando cuando el programa sale, pero se termina.
  5. 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.

Resumen

En este laboratorio, ha aprendido las técnicas esenciales para trabajar con hilos de Python y cómo esperar a que se completen. Aquí hay un resumen de los conceptos clave cubiertos:

  1. Creación e Inicio de Hilos: Aprendió a crear un objeto de hilo, especificar la función objetivo e iniciar su ejecución con el método start().

  2. Esperando a los Hilos con join(): Descubrió cómo usar el método join() para esperar a que un hilo se complete antes de continuar con el programa principal, asegurando una sincronización adecuada.

  3. Trabajando con Múltiples Hilos: Practicó la creación y gestión de múltiples hilos, tanto manualmente como utilizando la clase ThreadPoolExecutor para una gestión de hilos más eficiente.

  4. Tiempos de Espera de Hilos y Hilos Demonios: Exploró temas avanzados, incluyendo la configuración de tiempos de espera para las operaciones de hilos y el uso de hilos demonios para tareas en segundo plano.

Estas habilidades proporcionan una base para el desarrollo de aplicaciones con múltiples hilos en Python. El multihilo permite que sus programas realicen múltiples tareas concurrentemente, mejorando el rendimiento y la capacidad de respuesta, especialmente para operaciones vinculadas a E/S (I/O-bound operations).

A medida que continúe trabajando con hilos, recuerde estas mejores prácticas:

  • Use hilos para tareas vinculadas a E/S, no para tareas vinculadas a la CPU (considere usar multiprocesamiento para estas últimas)
  • Sea consciente de los recursos compartidos y use mecanismos de sincronización apropiados
  • Considere usar abstracciones de nivel superior como ThreadPoolExecutor para gestionar múltiples hilos
  • Use hilos demonios para tareas en segundo plano que no deberían impedir que el programa salga

Con estas habilidades y prácticas, ahora está equipado para construir aplicaciones Python más eficientes y con mayor capacidad de respuesta utilizando técnicas de multihilo.