From 8a2d82fe6ff54b09d875f6df888edcd504651806 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 11:46:53 +0000 Subject: [PATCH] feat: Add Lesson 11 - Concurrency and Parallelism - Implement `sensors.py` to demonstrate Threading for I/O bound tasks. - Implement `analytics.py` to demonstrate Multiprocessing for CPU bound tasks. - Create `robot.py` to orchestrate both paradigms. - Add `benchmark_gil.py` to empirically prove GIL limitations on threads vs processes. - Add comprehensive README.md with theory and instructions. --- 11_concurrency_parallelism/README.md | 45 ++++++++++++++ 11_concurrency_parallelism/analytics.py | 45 ++++++++++++++ 11_concurrency_parallelism/benchmark_gil.py | 66 +++++++++++++++++++++ 11_concurrency_parallelism/robot.py | 44 ++++++++++++++ 11_concurrency_parallelism/sensors.py | 51 ++++++++++++++++ 5 files changed, 251 insertions(+) create mode 100644 11_concurrency_parallelism/README.md create mode 100644 11_concurrency_parallelism/analytics.py create mode 100644 11_concurrency_parallelism/benchmark_gil.py create mode 100644 11_concurrency_parallelism/robot.py create mode 100644 11_concurrency_parallelism/sensors.py diff --git a/11_concurrency_parallelism/README.md b/11_concurrency_parallelism/README.md new file mode 100644 index 0000000..423a891 --- /dev/null +++ b/11_concurrency_parallelism/README.md @@ -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. diff --git a/11_concurrency_parallelism/analytics.py b/11_concurrency_parallelism/analytics.py new file mode 100644 index 0000000..84b142c --- /dev/null +++ b/11_concurrency_parallelism/analytics.py @@ -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 diff --git a/11_concurrency_parallelism/benchmark_gil.py b/11_concurrency_parallelism/benchmark_gil.py new file mode 100644 index 0000000..c53706a --- /dev/null +++ b/11_concurrency_parallelism/benchmark_gil.py @@ -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) diff --git a/11_concurrency_parallelism/robot.py b/11_concurrency_parallelism/robot.py new file mode 100644 index 0000000..d807111 --- /dev/null +++ b/11_concurrency_parallelism/robot.py @@ -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() diff --git a/11_concurrency_parallelism/sensors.py b/11_concurrency_parallelism/sensors.py new file mode 100644 index 0000000..b9c90d5 --- /dev/null +++ b/11_concurrency_parallelism/sensors.py @@ -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()