UP | HOME

Hilos en Python

Table of Contents

Python proporciona concurrencia por medio de hilos a través del módulo threading (Python Threading). Este permite manejar y gestionar hilos, haciendo posible la ejecución paralela de tareas compartiendo el espacio de memoria.

Recuerda que los hilos de un mismo proceso comparten el espacio de memoria.

Los hilos son particularmente útiles para tareas de de E/S (Entrada/Salida), como son operaciones con ficheros y peticiones por red, donde gran parte del tiempo se gasta esperando recursos externos.

Procesos vs Hilos

Recuerda, del tema anterior, que los procesos y los hilos están relacionados. De hecho, un hilo es parte de un proceso, ya que es una unidad de ejecución de un proceso. Un proceso tiene, siempre, un hilo inicial llamado hilo principal. A partir de aquí, los procesos, pueden crear más hilos que ejecuten tareas concretas explotando, así, las capacidades de concurrencia y paralelismo de los sistemas actuales.

Ciclo de vida de un hilo

  • Nuevo hilo, un hilo creado con threading.Thread
  • Hilo en ejecución, que puede bloquearse y pasar al estado de hilo bloqueado
  • Hilo terminado

estados_hilo.png

Figure 1: Estados de un hilo

Hilos en Python

Dentro del módulo threading tenemos una clase para crear y manejar hilos en Python: Thread. Esta clase se usa de forma similar a la clase Process del módulo multiprocessing estudiado en el tema anterior. Así, son APIs homogéneas que hace sencillo usar ambas opciones.

Uso básico de hilos:

  1. Crea un hilo:
from threading import Thread
import time


def f(delay):
    time.sleep(delay)


thread = Thread(target=f, args(3,))
  1. Ejecútalo:
thread.start()
  1. Espera a que el hilo finalice:
thread.join()

Ejemplos básicos

Ejecución de una función en un hilo

Veamos el ejemplo básico que encontramos en la documentación oficial de Python, introducción a los hilos en Python, en el que tenemos:

  • Una lista de enlaces sobre las que vamos a simular un crawler (función crawl)
  • Por cada enlace se crea un hilo que hará el scraping de dicho enlace
  • Como en el caso de los programas multiproceso, se lanzan los hilos con el método start() y se espera a que finalicen con el método join()
import threading
import time

links = [
    "https://python.org",
    "https://docs.python.org",
    "https://peps.python.org",
]


def crawl(link, delay=3):
    print(f"crawl started for {link}")
    time.sleep(delay)  # Blocking I/O (simulando petición por red)
    print(f"crawl ended for {link}")


# Se crea un hilo por cada enlace (link)
threads = []
for link in links:
    t = threading.Thread(target=crawl, args=(link, 2))
    threads.append(t)

# Se da comienzo a todos los hilos anteriormente creados
for t in threads:
    t.start()

# Esperamos que finalicen todos los hilos
for t in threads:
    t.join()

Extendiendo la clase Thread

Como en el caso de los procesos, con los hilos también puedes crear tu propia clase que herede de Thread y sobreescriba el método run() que es el método que se ejecutará cuando se llame el método start() del hilo creado.

Para entenderlo, aquí tienes el ejemplo anterior usando una clase derivada de Thread:

import threading
import time


class WebCrawler(threading.Thread):
    def __init__(self, link, delay):
        super().__init__()
        self.__link = link
        self.__delay = delay

    def run(self):
        print(f"crawl started for {self.__link}")
        time.sleep(self.__delay)  # Simulando petición por red
        print(f"crawl ended for {self.__link}")


if __name__ == "__main__":
    links = [
        "https://python.org",
        "https://docs.python.org",
        "https://peps.python.org",
    ]
    threads = []
    for link in links:
        t = WebCrawler(link=link, delay=2)
        threads.append(t)

    for t in threads:
        t.start()

    for t in threads:
        t.join()

Métodos, atributos y utilidades de la clase Thread

Existen métodos y atributos de la clase Thread que puedes usar en tus programas y que resultarán útiles en diferentes contextos. Como siempre, en la documentación oficial tienes todos los detalles, aquí te resumo y destaco solo parte de lo que te proporciona la clase Thread.

Para todos estos ejemplos vamos a suponer que hemos creado un hilo llamado thread:

  • thread.ident: cada hilo tiene un identificador que le asigna el intérprete de Python y que está en la propiedad ident.
  • thread.native_id: la propiedad native_id contiene el identificador del hilo que le asigna el sistema operativo (identificador nativo del hilo).
  • thread.is_alive(): el método is_alive() devuelve True si el hilo está activo (en ejecución) o False si está dead (terminó su ejecución).
  • thread.name: los hilos tienen un nombre y este nombre se puede asignar al construir el hilo, por ejemplo, el siguiente código crea un hilo y le asigna un nombre de forma explícita vía argumento:
thread = Thread(name="Mi hilo", target=func)

A continuación, se listan algunas funciones y utilidades de los hilos. Todas estas funciones las tienes que importar desde el módulo threading:

  • current_thread(): esta función devuelve la referencia del hilo actual.
  • main_thread(): esta otra función devuelve la referencia del hilo principal.
  • active_count(): esta función devuelve el número de hilos activos del proceso actual.

Otra utilidad básica es la de poder enumerar todos los hilos para obtener información de los mismos. Se usa la función enumerate() del módulo threading. Por ejemplo, el siguiente programa muestra el nombre de todos los hilos activos:

import threading


threads = threading.enumerate()
for t in threads:
    print(t.name)

¿Cuándo usar hilos?

A estas alturas ya has desarrollado programas multiproceso y te has introducido en la programación multihilo. En Python, como te has dado cuenta, desarrollar programas multiproceso y multihilo es bastante parecido, así que, ahora, posiblemente, te estarás preguntando: ¿cuándo usar procesos y cuándo usar hilos? Te lo respondo aludiendo a la documentación oficial:

Threads are particularly useful when tasks are I/O bound, such as file operations or making network requests, where much of the time is spent waiting for external resources.

Es decir: deberías usar hilos para tareas en las que se realicen operaciones de Entrada/Salida que son tareas bloqueantes.

De esta manera, mientras un hilo está bloqueado esperando un recurso, el resto de hilos pueden seguir con sus tareas y, especialmente, el hilo principal, que debería permanecer siempre activo, sobre todo en aplicaciones con interfaces gráficas de usuario, ya que, como he explicado en clase, el hilo principal de las aplicaciones gráficas es el encargado de mantener "viva" la interfaz gráfica para que el usuario pueda interactuar con ella en todo momento.

Así pues, cuando tengas que realizar programas en los que se tengan que llevar a cabo tareas intensivas de CPU, ahí, en ese caso, deberías optar por usar procesos y explotar la concurrencia y el paralelismo por medio de la programación multiproceso.

Uso de ThreadPoolExecutor

Existe una manera más sencilla de dar comienzo a un conjunto de hilos por medio de una clase llamada ThreadPoolExecutor, que se encuentra en el módulo concurrent.futures de Python.

Esta clase facilita el desarrollo de programas multihilo como el que hemos visto anteriormente. De hecho, para entender cómo se usa un Pool Executor vamos a reescribir nuestro crawler:

import time

from concurrent.futures import ThreadPoolExecutor


def crawl(link, delay):
    print(f"crawl started for {link}")
    time.sleep(delay)
    print(f"crawl ended for {link}")


if __name__ == "__main__":
    links = [
        "https://python.org",
        "https://docs.python.org",
        "https://peps.python.org",
    ]

    with ThreadPoolExecutor(max_workers=2) as executor:
        for link in links:
            executor.submit(crawl, link, 3)

La forma más sencilla de usar un ThreadPoolExecutor es por medio de un Context Manager (with) de Python. De esta manera no nos tenemos que preocupar de manejar la finalización de los hilos como hacíamos en el ejemplo anterior con join().

El ThreadPoolExecutor necesita que le pasemos el número de workers (hilos) que se van a lanzar concurrentmente. En nuestro ejemplo hemos decidido que vamos a ejecutar los hilos de 2 en 2. Así, nunca habrán más de dos hilos ejecutándose en paralelo.

En este ejemplo, executor es el objeto de tipo ThreadPoolExecutor y, si leemos la documentación, hay varias maneras de lanzar los workers (hilos), una de ellas es por medio del método submit que recibe:

  • Como primer argumento la función que cada worker (hilo) va a ejecutar.
  • El resto de argumentos son, a su vez, los argumentos que se pasan a la función indicada anteriormente. En nuestro caso, la función crawl necesita dos argumentos: el enlace, en primer lugar, y el delay en segundo lugar.

Futuros en Python

Es posible que te haya llamado la atención que la clase ThreadPoolExecutor se encuentre en un módulo llamado concurrent.futures y no en el módulo threading. Esto se debe a que ThreadPoolExecutor está estrechamente vinculada a lo que, en Python, se conoce como Futuros.

Un futuro en Python es una característica del módulo concurrent.futures que permite realizar operaciones asíncronas y manejar el resultado de operaciones que pueden tardar en completarse, como tareas que implican I/O o computación intensiva. Es habitual usarse en programas multihilo.

Un futuro es un objeto que representa el resultado de una operación que puede completarse en el futuro. Permite que el programa siga ejecutándose mientras se espera el resultado.

El módulo concurrent.futures proporciona dos tipos principales de ejecutores que permiten trabajar con futuros, uno de los cuales es ThreadPoolExecutor.

Su funcionamiento es básico: cuando se envía una tarea a un executor, devuelve un objeto de tipo concurrent.futures.Future. Con este objeto puedes:

  • Consultar su estado (si está en ejecución, completado o fallido).
  • Obtener el resultado con el método result(), el cual bloqueará el hilo hasta que la tarea esté completada.

Ejemplo con manejo de resultado

Vamos a reescribir el ejemplo anterior modificando la función crawl para que devuelva la página HTML que será recogida por medio del objeto concurrent.futures.Future.

import urllib.request

from concurrent.futures import ThreadPoolExecutor, as_completed


def crawl(link, timeout):
    with urllib.request.urlopen(link, timeout=timeout) as conn:
        return conn.read()


if __name__ == "__main__":
    links = [
        "https://python.org",
        "https://docs.python.org",
        "https://peps.python.org",
    ]

    with ThreadPoolExecutor(max_workers=2) as executor:
        futures = []

        for link in links:
            future = executor.submit(crawl, link, 3)
            futures.append(future)

        for future in as_completed(futures):
            try:
                data = future.result()
                print(f"Página leída: {len(data)} bytes")
            except Exception as e:
                print(f"Excepción: {e}")

Author: Román Ginés Martínez Ferrández

Created: 2026-01-09 vie 20:58

Validate