Módulo AVANZADOLección 03 de 06

Concurrencia: threads y procesos

threading, multiprocessing y el GIL: cuándo usar cada uno.

01

Teoría y Fundamentos

Lee y asimila los conceptos clave antes de evaluar

Cuando tu programa espera red, disco o calcula demasiado, la concurrencia le deja hacer otras cosas mientras tanto. Python ofrece dos caminos: threading (hilos) y multiprocessing (procesos) — y elegir mal es el error clásico.

El GIL: la regla del juego

El GIL (Global Interpreter Lock) es un candado del intérprete estándar de Python: solo un hilo ejecuta bytecode de Python a la vez. Dos hilos no calculan en paralelo real.

Entonces, ¿para qué sirven los hilos? El GIL se libera durante esperas de I/O (red, disco, sleep). Mientras un hilo espera, otro puede correr. Ahí está el truco:

  • I/O-bound (esperar red/disco): hilos y asyncio ayudan muchísimo
  • CPU-bound (calcular): necesitas procesos, cada uno con su GIL propio

threading: hilos para esperar mejor

import threading
import time

def descargar(nombre):
    print(f"descargando {nombre}...")
    time.sleep(1)                    # simula espera de red
    print(f"{nombre} lista")

inicio = time.perf_counter()

hilos = [
    threading.Thread(target=descargar, args=("archivo1",)),
    threading.Thread(target=descargar, args=("archivo2",)),
]
for hilo in hilos:
    hilo.start()                     # arrancar
for hilo in hilos:
    hilo.join()                      # esperar a que terminen

print(f"total: {time.perf_counter() - inicio:.1f}s")   # ~1s, no 2s

Dos descargas de 1 segundo terminan en ~1 segundo: mientras una espera, la otra corre. Sin hilos serían 2 segundos.

Compartir datos: la zona peligrosa

Los hilos comparten memoria. Si dos hilos modifican lo mismo a la vez, aparecen condiciones de carrera:

import threading

contador = 0
lock = threading.Lock()

def incrementar():
    global contador
    for _ in range(100_000):
        with lock:           # solo un hilo a la vez en esta sección
            contador += 1    # leer + sumar + escribir NO es atómico

hilos = [threading.Thread(target=incrementar) for _ in range(2)]
for h in hilos: h.start()
for h in hilos: h.join()

print(contador)   # 200000 con lock; SIN lock, un número impredecible

Regla: datos compartidos entre hilos → protégelos con Lock (o evita compartir).

multiprocessing: paralelismo real

from multiprocessing import Pool

def es_primo(n):
    if n < 2:
        return False
    for divisor in range(2, int(n ** 0.5) + 1):
        if n % divisor == 0:
            return False
    return True

if __name__ == "__main__":        # ¡obligatorio en Windows/Mac!
    with Pool(processes=4) as pool:
        resultados = pool.map(es_primo, range(2, 100))
    print(resultados.count(True))

Pool.map reparte la lista entre procesos que corren en núcleos distintos, en paralelo real. Cada proceso tiene su intérprete y su GIL — ideal para CPU-bound.

Ojo: los procesos NO comparten memoria (se comunican serializando datos). Y el bloque if __name__ == "__main__": es obligatorio para el arranque de Pool en la mayoría de plataformas.

concurrent.futures: la API moderna

Una interfaz unificada para ambos mundos:

from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

with ThreadPoolExecutor(max_workers=5) as executor:
    resultados = list(executor.map(descargar, ["a", "b", "c"]))

# mismo código con ProcessPoolExecutor para CPU-bound

En código nuevo, usa concurrent.futures: cambia de hilos a procesos cambiando una clase.

La tabla de decisión

Tu tareaHerramienta
Espera red/disco (muchas tareas)ThreadPoolExecutor o asyncio
Cálculo pesado en CPUProcessPoolExecutor
Pocas tareas rápidasSecuencial — no compliques

Resumen

  • El GIL permite un solo hilo ejecutando Python a la vez; se libera en esperas de I/O
  • Hilos para I/O-bound; procesos para CPU-bound
  • Datos compartidos entre hilos → threading.Lock, o no los compartas
  • concurrent.futures es la API moderna unificada
  • La concurrencia agrega complejidad: úsala solo cuando la espera sea medible

¿Terminaste de leer la teoría?

Marca este paso para registrar tu avance y saltar a las preguntas

02

Quiz de Comprobación

3 preguntas de comprensión rápida (requiere 70% para aprobar)

  1. 1

    ¿Qué es el GIL de Python?

  2. 2

    Tu tarea es descargar 100 URLs (esperar red). ¿Qué enfoque conviene?

  3. 3

    Tu tarea es procesar 100 imágenes con cálculo pesado (CPU). ¿Qué conviene?

03

Laboratorio de Código Python

2 ejercicios interactivos ejecutados en tu navegador

EJERCICIO 01Python 3.12

Hilos en acción

Usa threading.Thread para ejecutar dos funciones que imprimen su nombre, arranca ambas con start() y espera con join(). Salida esperada en dos líneas.

ejercicio_1.py
Tab: 4 spaces

Iniciando editor de código…

Ctrl/Cmd + Enter para ejecutar
EJERCICIO 02Python 3.12

Pool de procesos

Usa multiprocessing.Pool (o concurrent.futures.ProcessPoolExecutor) para calcular cubo(x) para [1, 2, 3, 4] en paralelo e imprimir la lista de resultados. Resultado: [1, 8, 27, 64]

ejercicio_2.py
Tab: 4 spaces

Iniciando editor de código…

Ctrl/Cmd + Enter para ejecutar