Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions 11_concurrency_parallelism/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Lección 11: Concurrencia y Paralelismo — El Motor de la Robótica 🤖

¡Bienvenido al Nivel Pro!

En esta lección entramos en el mundo de la **ejecución simultánea**. Si quieres construir robots, procesar Big Data o manejar miles de conexiones web, no puedes vivir en un mundo "single-threaded".

## 🧠 Teoría: ¿Por qué mi código es lento?

### 1. El mito de la multitarea
Python tiene un "guardián" llamado **GIL (Global Interpreter Lock)**. Este cerrojo asegura que solo un hilo (thread) ejecute código Python a la vez dentro de un mismo proceso.

* **¿Entonces los Threads no sirven?** ¡Sí sirven! Pero solo para tareas **I/O Bound** (esperar red, esperar disco, esperar input de usuario). Mientras un hilo espera, suelta el GIL y otro puede trabajar.
* **¿Y si quiero usar todos los núcleos de mi CPU?** Necesitas **Multiprocessing**. Al crear nuevos procesos, cada uno tiene su propia instancia de Python y su propio GIL. ¡Libertad real!

### 2. Threading vs Multiprocessing (Regla de Oro)

| Herramienta | Tipo de Tarea | Ejemplo Robótica/Data | Coste de Memoria |
| :--- | :--- | :--- | :--- |
| **Threading** | **I/O Bound** (Esperar) | Leer sensores, peticiones HTTP, DB queries | Bajo (comparten memoria) |
| **Multiprocessing** | **CPU Bound** (Calcular) | Procesar visión por computador, ML training | Alto (memoria separada) |

---

## 🧰 Hands-On: Simulador de Robot "NeuroBot"

Vamos a construir el núcleo de un robot que necesita hacer dos cosas a la vez:
1. **Leer Sensores (I/O Bound):** Simulado con `time.sleep()`. Usaremos **Threads** para no bloquear el sistema.
2. **Analizar Datos (CPU Bound):** Cálculos matemáticos pesados. Usaremos **Multiprocessing** para no congelar los sensores.

### Estructura del Proyecto

* `sensors.py`: Módulo de lectura de sensores (Threading).
* `analytics.py`: Módulo de procesamiento pesado (Multiprocessing).
* `robot.py`: El cerebro que coordina todo.
* `benchmark_gil.py`: **Test Ninja** para demostrar la supremacía de los Procesos en CPU bound.

---

## 🧪 Test Ninja (Benchmark)

Ejecuta `python benchmark_gil.py` para ver con tus propios ojos cómo el GIL afecta el rendimiento.

## 🚀 Ejecución del Robot

Ejecuta `python robot.py` para ver el sistema en acción.
45 changes: 45 additions & 0 deletions 11_concurrency_parallelism/analytics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import multiprocessing
import time
import math

def cpu_heavy_task(data_chunk: int) -> int:
"""
Simula una tarea intensiva de CPU (ej. análisis de imagen, cálculo matricial).
Calcula la suma de factoriales (intencionalmente ineficiente para estresar la CPU).
"""
result = 0
# Aumentamos la complejidad para que se note el esfuerzo
for i in range(1, 2000):
result += math.factorial(i % 50)
return result

class AnalyticsEngine:
def __init__(self):
pass

def process_data_parallel(self, data_chunks: list[int]) -> list[int]:
"""
Procesa una lista de datos utilizando múltiples procesos.
Cada proceso tiene su propio intérprete de Python y GIL.
"""
print(f"\n🧠 Iniciando Análisis Pesado con {multiprocessing.cpu_count()} núcleos...")
start_time = time.time()

# Pool de procesos: Python gestiona los workers automáticamente
with multiprocessing.Pool() as pool:
results = pool.map(cpu_heavy_task, data_chunks)

duration = time.time() - start_time
print(f"✅ Análisis completado en {duration:.4f} segundos.")
return results

def process_data_serial(self, data_chunks: list[int]) -> list[int]:
"""
Procesa los datos secuencialmente (para comparar).
"""
print("\n🐌 Iniciando Análisis Secuencial (un solo núcleo)...")
start_time = time.time()
results = [cpu_heavy_task(d) for d in data_chunks]
duration = time.time() - start_time
print(f"✅ Análisis completado en {duration:.4f} segundos.")
return results
66 changes: 66 additions & 0 deletions 11_concurrency_parallelism/benchmark_gil.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import time
import threading
import multiprocessing
import math

def heavy_computation():
"""Calcula suma de potencias, tarea puramente de CPU."""
count = 0
for i in range(10**6):
count += math.pow(i, 2)
return count

def run_threads(n_tasks):
print(f"🧵 Ejecutando {n_tasks} tareas con THREADS...")
start = time.time()
threads = []
for _ in range(n_tasks):
t = threading.Thread(target=heavy_computation)
threads.append(t)
t.start()

for t in threads:
t.join()
return time.time() - start

def run_processes(n_tasks):
print(f"🏭 Ejecutando {n_tasks} tareas con PROCESOS...")
start = time.time()
processes = []
for _ in range(n_tasks):
p = multiprocessing.Process(target=heavy_computation)
processes.append(p)
p.start()

for p in processes:
p.join()
return time.time() - start

if __name__ == "__main__":
TASKS = 4 # Número de tareas concurrentes
print("--- ⚔️ BENCHMARK: GIL vs MULTIPROCESSING ⚔️ ---")
print(f"Tareas: {TASKS} cálculos pesados.")

# 1. Test secuencial (Línea base)
print(f"🐌 Ejecutando secuencialmente (Línea Base)...")
start = time.time()
for _ in range(TASKS):
heavy_computation()
time_seq = time.time() - start
print(f"⏱️ Tiempo Secuencial: {time_seq:.4f}s\n")

# 2. Test Threading
time_threads = run_threads(TASKS)
print(f"⏱️ Tiempo Threads: {time_threads:.4f}s")
print(" (Nota: Si es similar o peor que secuencial, es culpa del GIL)\n")

# 3. Test Multiprocessing
time_processes = run_processes(TASKS)
print(f"⏱️ Tiempo Procesos: {time_processes:.4f}s")

# Conclusión
improvement = time_threads / time_processes
print("-" * 40)
print(f"🏆 GANADOR: {'PROCESOS' if time_processes < time_threads else 'THREADS'}")
print(f"🚀 Factor de aceleración: {improvement:.2f}x más rápido")
print("-" * 40)
44 changes: 44 additions & 0 deletions 11_concurrency_parallelism/robot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import time
from sensors import Sensor, SensorManager
from analytics import AnalyticsEngine

def main():
print("🤖 Iniciando Protocolo de Arranque del NeuroBot v1.0")

# 1. Configuración de Sensores (I/O Bound -> Threading)
sensor_mgr = SensorManager()
sensor_mgr.add_sensor(Sensor("Lidar", 1.0))
sensor_mgr.add_sensor(Sensor("Termico", 2.5))

# 2. Arrancar hilos de sensores (no bloquean el main thread)
sensor_mgr.start_all()

# Dejamos que los sensores trabajen un poco mientras el robot "hace otras cosas"
print("\n... El robot está 'pensando' mientras los sensores recogen datos ...")
time.sleep(3)

# 3. Ejecutar Tarea Pesada (CPU Bound -> Multiprocessing)
# Imaginemos que hemos acumulado 10 lotes de datos para procesar
data_payload = [100] * 8 # 8 tareas pesadas

engine = AnalyticsEngine()

# El procesamiento pesado ocurre en paralelo en otros núcleos
# Nota: En una app real, esto podría ser async o en background,
# pero aquí bloquearemos el main thread para ver el resultado,
# ¡sin embargo, los sensores siguen reportando en sus hilos!
results = engine.process_data_parallel(data_payload)

print(f"📊 Resultado del análisis: {results[:2]}... (total {len(results)})")

# Dejamos correr un poco más
time.sleep(2)

# 4. Apagar
sensor_mgr.stop_all()
print("🤖 NeuroBot apagado correctamente.")

if __name__ == "__main__":
# Protección necesaria para multiprocessing en Windows/macOS,
# buena práctica siempre en Python.
main()
51 changes: 51 additions & 0 deletions 11_concurrency_parallelism/sensors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import threading
import time
import random
from typing import List, Dict

class Sensor:
def __init__(self, name: str, interval: float):
self.name = name
self.interval = interval
self.running = False
self.data: float = 0.0
self._thread = threading.Thread(target=self._run_loop, name=f"Thread-{name}")

def start(self):
"""Inicia la lectura del sensor en un hilo separado."""
self.running = True
print(f"🔌 Sensor {self.name} activado.")
self._thread.start()

def stop(self):
"""Detiene el sensor."""
self.running = False
self._thread.join()
print(f"📴 Sensor {self.name} detenido.")

def _run_loop(self):
"""Simula operación I/O bound (esperar datos)."""
while self.running:
# Simula tiempo de espera de I/O (lectura de hardware)
time.sleep(self.interval)

# Simula obtención de dato
self.data = round(random.uniform(20.0, 30.0), 2)
print(f" [{self.name}] Dato leído: {self.data}")

class SensorManager:
def __init__(self):
self.sensors: List[Sensor] = []

def add_sensor(self, sensor: Sensor):
self.sensors.append(sensor)

def start_all(self):
print("\n--- Iniciando Sistema de Sensores (Threading) ---")
for s in self.sensors:
s.start()

def stop_all(self):
print("\n--- Deteniendo Sistema de Sensores ---")
for s in self.sensors:
s.stop()