# Hacer muchas cosas a la vez

Curso exprés sobre concurrency y async. 19 conceptos — Concurrency y Parallelism, Process y Thread, Context Switch, Blocking y Async/Await, Event Loop, Race Condition, Lock, Deadlock, Message Passing, Immutability, Atomic Operation y más — cada uno con su nombre en inglés, una imagen, un diagrama y una ficha con un ejemplo y un coste. Al final: un mapa de relaciones y una chuleta de una página. La idea central: hacer muchas cosas a la vez es como el software se mantiene rápido, y los errores vienen todos de dos cosas que tocan los mismos datos al mismo tiempo.

- **Author:** Fedor Chishchin
- **Level:** intermediate
- **Reading time:** ~30 minutos de lectura
- **Published:** 2026-06-13
- **Source:** https://fedorthinks.com/es/learn/concurrency-express

---

## 1. Concurrency frente a parallelism

Dos palabras se usan como si significaran lo mismo, y no es así. Una trata de hacer malabares; la otra, de tener muchas manos. Distinguirlas es la base de todo lo demás.

### El mapa: 19 conceptos en seis grupos

> Un mapa de metro: primero ves las líneas enteras, luego recorres estación por estación.

Primero lo **básico**: qué son concurrency, parallelism y un core de CPU. Después cinco grupos, uno por capítulo: **threads y processes**, **la espera** (blocking, async, el event loop), el **estado compartido** y las races, los **locks** y sus peligros, y los **patrones más seguros** que eliminan la race de raíz.

_Diagram: Seis grupos de conceptos — los capítulos 1–6 los siguen. El capítulo 7 lo une todo._

### Cómo leer cada concepto

> Un pasaporte: los mismos campos para todos, así es fácil compararlos.

Cada concepto de abajo recibe el mismo trato: su **nombre en inglés** (apréndelo: es el término con el que buscarás en la documentación), una imagen de la vida real, un **diagrama** y una **ficha** con estos campos: _problema_, _idea_, _ejemplo_, _coste_ y conceptos _relacionados_. Al final: un mapa de relaciones y una chuleta.

### Concurrency — hacer malabares con tareas

> Un cocinero atendiendo cuatro platos — corta, luego remueve, luego revisa el horno, cambiando rápido entre ellos.

**Concurrency** es ocuparse de muchas tareas alternando entre ellas — un solo trabajador avanzando en varias cosas, pero sin hacer nunca dos en el mismo instante. Un cocinero haciendo malabares con cuatro platos es concurrency. Es una forma de **estructurar** el trabajo.

_Diagram: Un trabajador, cuatro tareas: cambia rápido y todas avanzan — pero solo una se ejecuta en cada instante._

#### Concurrency (malabares por alternancia)

- **Problema:** Muchas tareas, un trabajador — y el trabajador pierde tiempo esperando.
- **Idea:** Alternar entre tareas para que todas avancen.
- **Ejemplo:** Un cocinero lleva cuatro platos, un paso de cada uno por turnos.
- **Coste:** Complejidad: las tareas se entrelazan y el orden de los pasos se vuelve impredecible.
- **Relaciones:** A menudo se combina con **Parallelism**, pero es otra idea. La base de **Async** y del **Event Loop**.

### Parallelism — muchas manos

> Cuatro cocineros, cada uno haciendo su propio plato en el mismo instante.

**Parallelism** es muchas tareas ejecutándose literalmente en el mismo instante, en varios trabajadores. Cuatro cocineros es parallelism. Es una forma de **ejecutar** el trabajo. Concurrency y parallelism a menudo se combinan, pero son ideas distintas: la primera trata de cómo organizas el trabajo, la segunda de cuántas manos lo hacen.

_Diagram: Cuatro cores, cuatro tareas — en el mismo instante, las cuatro se ejecutan de verdad._

#### Parallelism (ejecutar en el mismo instante)

- **Problema:** Hay tanto trabajo que un trabajador no basta, aunque nunca espere.
- **Idea:** Ejecutar tareas en el mismo instante en varios trabajadores (cores).
- **Ejemplo:** Un cálculo grande dividido en trozos usa todos los cores de la máquina.
- **Coste:** Necesita varios cores; los datos compartidos entre ellos se vuelven peligrosos.
- **Relaciones:** La cura para el trabajo **CPU-bound**. Se construye sobre **CPU Core**, **Thread** y **Process**.

### CPU Core — un core es concurrente, muchos cores son paralelos

> Un solo empleado de taquilla puede atender una cola entera ocupándose por turnos de los pasos rápidos de cada persona; una fila de empleados atiende a varias personas en el mismísimo segundo.

Un **core** de procesador hace una cosa a la vez, pero puede cambiar entre tareas tan rápido que parece hacer muchas — eso es concurrency en un solo core. El **parallelism** de verdad necesita varios cores ejecutándose realmente a la vez. Las máquinas modernas tienen muchos cores, así que el software real suele hacer ambas cosas: varias tareas en curso (concurrente), algunas ejecutándose de verdad juntas (paralelo).

_Diagram: Dentro de cada core, las tareas se turnan (concurrency); los dos cores se ejecutan al mismo tiempo (parallelism)._

#### CPU Core (core del procesador)

- **Qué es:** Un ejecutor del procesador: hace una sola cosa en cada instante.
- **Cómo funciona:** Cambia entre tareas muy rápido — por eso parece que hace muchas.
- **Ejemplo:** Una máquina con 8 cores puede hacer de verdad 8 cosas en el mismo instante.
- **Por qué importa:** El número de cores explica qué aceleraciones son siquiera posibles.
- **Relaciones:** Un core da **Concurrency**, muchos cores dan **Parallelism**. Cambiar de tarea cuesta un **Context Switch**.

### Por qué molestarse siquiera

> Un camarero que tomara el pedido completo de una mesa, lo sirviera y solo entonces saludara a la siguiente mesa dejaría una sala llena de gente esperando sin motivo.

Hacer una cosa a la vez desperdicia enormes cantidades de espera. Mientras una tarea espera — una respuesta de red, un disco, un usuario — el trabajador podría estar avanzando en otra. Concurrency recupera ese tiempo muerto; parallelism añade potencia bruta para el trabajo pesado. Juntas son la razón de que una app siga respondiendo mientras carga, y de que un cálculo grande pueda usar todos los cores. El coste es la complejidad de la que trata el resto de este curso.

_Diagram: De una en una: el trabajador se queda parado mientras espera. Solapadas: mientras A espera, B ya se está ejecutando — y todo termina antes._

> **La pregunta de fondo:** «¿Necesito que un trabajador deje de perder tiempo esperando (concurrency), o de verdad más trabajadores ejecutándose a la vez (parallelism) — y qué pide realmente mi problema?»

> Concurrency es hacer malabares con muchas tareas alternando; parallelism es ejecutarlas en el mismo instante en muchos cores. Ideas distintas — una estructura el trabajo, la otra lo ejecuta.

## 2. Threads y processes

Para ejecutar cosas de forma concurrente, divides el trabajo en líneas de ejecución separadas. Hay dos tipos principales, y la diferencia entre ellos trata sobre todo de una cosa: si comparten memoria.

### Process — un programa aislado

> Dos cocinas separadas en dos edificios separados — cada una con sus ingredientes y sus encimeras, sin poder tocar las provisiones de la otra sin una llamada de teléfono de por medio.

Un **process** es un programa en ejecución con su propia **memoria privada**, aislada de otros processes. Dos processes no pueden corromper por accidente los datos del otro porque no comparten ninguno — para comunicarse tienen que pasarse mensajes a propósito. Ese aislamiento hace a los processes seguros y robustos, pero más pesados: arrancar uno cuesta más, y hablar entre ellos es más lento.

_Diagram: Cada process tiene su propia memoria. Hablan solo mediante mensajes — es seguro, pero más caro._

#### Process (programa aislado)

- **Problema:** Un programa podría corromper por accidente los datos de otro.
- **Idea:** Dar a cada programa su propia memoria privada, tras un muro.
- **Ejemplo:** Dos programas en ejecución en una máquina; hablan solo mediante mensajes.
- **Coste:** Más pesado: arrancar uno cuesta más, y hablar entre processes es más lento.
- **Relaciones:** Los **Threads** viven dentro de un process. Que los processes hablen entre sí es **Message Passing**.

### Thread — comparte memoria con sus hermanos

> Varios cocineros en una misma cocina, compartiendo las mismas encimeras e ingredientes — rápidos de coordinar, pero pueden echar mano del mismo cuchillo en el mismo momento.

Un **thread** es una línea de ejecución _dentro_ de un process, y los threads del mismo process **comparten la misma memoria**. Eso los hace ligeros y rápidos de coordinar — pero también es la fuente de casi todos los errores de concurrency, porque dos threads pueden tocar los mismos datos al mismo tiempo. La memoria compartida es un poder y un peligro: comunicación barata, colisiones peligrosas.

_Diagram: Tres threads de un mismo process miran todos la misma memoria compartida._

#### Thread (línea de ejecución)

- **Problema:** Los processes son pesados, y las partes de un programa necesitan compartir datos de cerca.
- **Idea:** Varias líneas de ejecución dentro de un process, compartiendo memoria.
- **Ejemplo:** Varios cocineros en una cocina, en las mismas encimeras.
- **Coste:** Dos threads pueden tocar los mismos datos a la vez — la raíz de casi toda race.
- **Relaciones:** La memoria compartida de los threads es **Shared Mutable State** — de ahí **Race Condition** y **Lock**.

### Context Switch — cambiar de tarea tiene un coste

> Un trabajador que hace malabares con tareas tiene que dejar una, recordar exactamente dónde estaba y coger otra — y todo ese dejar y coger lleva un tiempo real.

Cuando un core pasa de una tarea a otra, hace un **context switch**: guarda dónde estaba y carga el estado de la siguiente tarea. Es rápido pero no gratis, y hacerlo demasiado a menudo — miles de tareas diminutas agitándose — malgasta tiempo en pura sobrecarga. Por eso «simplemente crea más threads» no es automáticamente más rápido.

_Diagram: Unos pocos cambios no son nada. Miles de tareas diminutas — y el tiempo se va en el cambio mismo._

#### Context Switch (sobrecarga del cambio de tarea)

- **Qué es:** El core guarda el estado de una tarea y carga el de otra.
- **Ejemplo:** Un core alterna las tareas A y B, con una breve pausa de «guardar y cargar» entre ellas.
- **Ojo:** Con demasiadas tareas, los malabares cuestan más de lo que ahorran («thrashing»).
- **Conclusión:** «Más threads» no significa automáticamente «más rápido».
- **Relaciones:** El coste de **Concurrency** en un solo **CPU Core**; crece con el número de **Threads**.

> **La pregunta de los threads:** «¿Estas tareas necesitan compartir datos de cerca (threads, con el riesgo de colisión) o seguir aisladas con seguridad (processes, con la sobrecarga) — y estoy creando tantas que el cambio de tarea lo domina todo?»

> Los processes están aislados, con memoria privada y mensajería segura pero pesada. Los threads comparten memoria — ligeros y rápidos, y la raíz de la mayoría de los errores de concurrency.

## 3. Blocking y async

Buena parte del tiempo, un programa no está calculando — está esperando. Cómo manejes esa espera, bloqueante o asíncrona, decide si un trabajador puede mantenerse ocupado o se queda parado.

### Blocking Call — se detiene y espera

> Estar en un mostrador mientras el empleado va al almacén a buscar tu pedido, sin hacer nada hasta que vuelve — mientras estás ahí no puedes ayudar a nadie más.

Una llamada **blocking** es aquella en la que el trabajador se detiene y espera el resultado antes de hacer cualquier otra cosa — preguntar a la base de datos y quedarse ahí hasta que responda. Mientras está bloqueado, ese trabajador no logra nada. Para un solo paso rápido está bien; pero si bloqueas en cosas lentas — red, disco, otros servicios — desperdicias justo el tiempo que podrías haber dedicado a otro trabajo.

_Diagram: El trabajador envía una consulta y se queda parado hasta que vuelve la respuesta._

#### Blocking Call (parar y esperar)

- **Qué es:** Una llamada en la que el trabajador se detiene y espera el resultado.
- **Ejemplo:** Preguntar a la base de datos — y quedarse ahí hasta que responda.
- **Cuándo está bien:** Un solo paso rápido: casi no hay nada que esperar.
- **Coste:** En cosas lentas (red, disco, otros servicios) el trabajador pierde su tiempo.
- **Relaciones:** Lo opuesto de **Async / Await**.

### Async / Await — un trabajador sigue ocupado

> Coger un número en una charcutería llena: en vez de quedarte paralizado, empiezas otros recados y vuelves cuando llaman tu número.

El trabajo **asíncrono** (non-blocking) le da la vuelta: empiezas una operación lenta y, en vez de esperar, te pones con otra cosa y recoges el resultado cuando está listo. Las palabras clave **async** y **await** de muchos lenguajes expresan justo esto — «empieza esto, déjame seguir; vuelve cuando termine». Un solo trabajador puede tener cientos de operaciones lentas en curso a la vez. Así es como un único thread atiende a miles de conexiones en espera.

_Diagram: El trabajador inicia A, B y C, hace otro trabajo y recoge cada resultado cuando está listo._

#### Async / Await (asíncrono, non-blocking)

- **Problema:** Un trabajador blocking se queda parado mientras espera algo lento.
- **Idea:** Iniciar lo lento, no esperar — seguir; volver cuando esté listo.
- **Ejemplo:** Un script que hace muchas llamadas de red: con async se ejecutan juntas, no una tras otra.
- **Coste:** El código es más difícil de leer; solo ayuda donde hay espera.
- **Cuándo usarlo:** Trabajo **I/O-bound**: red, disco, otros servicios.
- **Relaciones:** Suele ejecutarse sobre un **Event Loop**. Lo opuesto de una **Blocking Call**.

### Event Loop — hace malabares con la espera

> Una recepcionista única y muy organizada que atiende cada llamada, lanza las consultas lentas y se ocupa de quien esté listo a continuación — sin bloquearse nunca con ninguna.

Async suele ejecutarse sobre un **event loop**: un trabajador que inicia tareas y, cada vez que termina algo lento, ejecuta el pequeño trozo de código que lo esperaba y sigue adelante. Parece que pasan muchas cosas a la vez, pero es un solo trabajador cambiando rápido entre tareas listas. Es el modelo de entornos como **JavaScript** y **Node** — asombrosamente eficiente para hacer malabares con la espera, en un solo thread.

_Diagram: El loop toma la siguiente tarea lista, lanza el trabajo lento y repite. Las respuestas terminadas se suman a la cola._

#### Event Loop (planificador de un solo thread)

- **Qué es:** Un thread que toma una y otra vez la siguiente tarea lista, en círculo.
- **Cómo funciona:** Lanza lo lento; cuando termina, ejecuta el pequeño trozo de código que lo esperaba.
- **Ejemplo:** JavaScript y Node: miles de esperas en un solo thread.
- **Ojo:** Un cálculo pesado ocupa ese único thread — y todos los demás esperan.
- **Relaciones:** El motor detrás de **Async / Await**; ideal para trabajo **I/O-bound**.

### I/O-bound vs CPU-bound — en qué espera tu trabajo

> Un trabajo que consiste sobre todo en esperar entregas se beneficia de un mejor malabarista; un trabajo que es sobre todo levantar peso necesita más músculo, no mejor planificación.

La herramienta correcta depende de en qué espera tu trabajo. El trabajo **I/O-bound** — que sobre todo espera red, disco u otros servicios — es perfecto para async: un trabajador hace malabares con toda la espera. El trabajo **CPU-bound** — cálculo pesado que mantiene un core totalmente ocupado — no recibe ayuda de async (no hay tiempo muerto que recuperar) y necesita **parallelism** entre cores. Juzgar mal cuál tienes es la razón de que algunas «aceleraciones» no hagan nada.

_Diagram: Izquierda: el trabajo sobre todo espera — async lo salva. Derecha: el core siempre está ocupado — necesitas más cores._

#### I/O-bound vs CPU-bound (mucha espera vs mucho cálculo)

- **I/O-bound:** Sobre todo espera de red, disco, otros servicios → **async**.
- **CPU-bound:** El cálculo pesado mantiene un core ocupado → **parallelism** entre cores.
- **Ejemplo:** Muchas llamadas de red — I/O-bound. Un cálculo grande en cada core — CPU-bound.
- **Ojo:** Si los confundes, la «aceleración» no hace nada.
- **Relaciones:** Elige entre **Async / Await** y **Parallelism**.

> **La pregunta de async:** «¿Este trabajo sobre todo espera (I/O-bound — recurre a async) o sobre todo calcula (CPU-bound — recurre a cores en paralelo)? Usar el equivocado no compra nada.»

> Blocking se detiene y espera; async inicia lo lento y sigue ocupado. Async gana en la espera (I/O-bound); el parallelism real gana en el cálculo pesado (CPU-bound).

## 4. El problema del estado compartido

Aquí está el corazón de por qué concurrency es difícil. En cuanto dos cosas pueden tocar los mismos datos al mismo tiempo, puede aparecer un error intermitente, invisible en las pruebas y exasperante de reproducir.

### Race Condition — dos manos, un objeto

> Dos personas alargando la mano hacia la última galleta en el mismo instante — las dos ven que está ahí, las dos la cogen, y lo que pase después depende del momento exacto, de una fracción de segundo.

Una **race condition** es cuando el resultado depende del momento preciso de operaciones concurrentes que tocan datos compartidos. Dos threads leen un contador como 5, los dos suman uno, los dos escriben 6 — y una cuenta se pierde en silencio, porque la respuesta correcta era 7. Nadie cometió un error de lógica; los pasos simplemente se entrelazaron mal. Este es el error que define a concurrency.

_Diagram: Los dos threads leen 5, los dos escriben 6. Esperábamos 7 — una actualización se perdió en silencio._

#### Race Condition (error que depende del momento)

- **Qué es:** El resultado depende del momento de las tareas sobre datos compartidos.
- **Ejemplo:** Dos threads leen el contador como 5, los dos escriben 6 — y la respuesta correcta era 7.
- **Causa:** Datos **compartidos** y **mutables** más acceso simultáneo.
- **Ojo:** No hay error de lógica — los pasos simplemente se entrelazaron en mal orden.
- **Relaciones:** Nace de **Shared Mutable State**. Se cura con **Lock**, **Message Passing**, **Immutability**, **Atomic Operation**.

### Non-determinism — lo terrible es que es intermitente

> Un traqueteo en el coche que desaparece justo cuando lo llevas al mecánico — real, intermitente e imposible de mostrar a demanda.

Las race conditions son odiosas porque son **non-deterministic**: dependen del momento, así que aparecen una vez cada mil ejecuciones, nunca en tus pruebas, y solo con carga real en producción. La misma entrada puede funcionar o fallar según una planificación microscópica. Por eso los errores de concurrency tienen una reputación temible — no puedes reproducirlos de forma fiable, y «en mi máquina funciona» es justo lo que esperarías incluso cuando está roto.

_Diagram: Mismo código, misma entrada: casi todas las ejecuciones pasan y una falla — por el momento._

#### Non-determinism (resultados que dependen del momento)

- **Qué es:** La misma entrada da un resultado distinto en distintas ejecuciones.
- **Por qué:** El resultado depende de la planificación microscópica de las tareas.
- **Ejemplo:** Un error aparece una vez cada mil ejecuciones — nunca en las pruebas, solo con carga en producción.
- **Ojo:** «En mi máquina funciona» es justo lo que esperarías, incluso cuando está roto.
- **Relaciones:** Lo que hace tan peligrosa a una **Race Condition**. Por eso es mejor hacer los errores imposibles que cazarlos.

### Shared Mutable State — el peligro real

> Las colisiones solo ocurren con las cosas que ambas personas pueden coger y cambiar — cierra con llave el armario compartido, o da a cada una el suyo, y las peleas se acaban.

Fíjate en la fuente exacta: datos que son a la vez **compartidos** (más de una tarea puede llegar a ellos) y **mutables** (se pueden cambiar). Quita cualquiera de las dos propiedades y la race desaparece — los datos que toca una sola tarea son seguros, y los datos que nadie cambia se pueden compartir libremente. Esta es la idea clave sobre la que se construye el resto del curso: toda solución es en realidad una forma de controlar, restringir o evitar el estado compartido mutable.

_Diagram: Solo una casilla de las cuatro es peligrosa: datos que son a la vez compartidos y mutables._

#### Shared Mutable State (datos compartidos + modificables)

- **Qué es:** Datos a los que llegan varias tareas y que se pueden cambiar.
- **Idea:** Quita «compartido» o «mutable» — y la race no tiene dónde vivir.
- **Ejemplo:** Un contador o una lista compartidos en los que escriben dos threads — peligroso. Datos de una sola tarea, o inmutables — seguros.
- **Conclusión:** Toda solución de este curso controla, restringe o elimina este estado.
- **Relaciones:** La fuente de **Race Condition**. **Lock** lo controla, **Message Passing** quita lo «compartido», **Immutability** quita lo «mutable».

> **La pregunta de la race:** «¿Qué dato pueden cambiar aquí dos tareas a la vez — y qué pasa si sus pasos se entrelazan en el peor orden posible?»

> Una race condition es dos tareas tocando a la vez datos compartidos y mutables, con el resultado colgando del momento. Es intermitente, invisible en las pruebas — y el error que define a concurrency.

## 5. Locks y deadlock

La solución clásica a una race es asegurarse de que solo una tarea toque los datos compartidos a la vez. Funciona — pero la herramienta que usas para imponerlo trae un peligro famoso propio.

### Lock / Mutex — acceso exclusivo para una tarea

> Una única llave de un baño de una sola persona: quien la tiene entra; todos los demás esperan su turno fuera. Nunca hay dos personas dentro a la vez.

Un **lock** (o **mutex**, de «mutual exclusion») asegura que solo una tarea entre a la vez en un tramo de código. Antes de tocar los datos compartidos, una tarea debe adquirir el lock; las demás que lo quieren esperan hasta que se libere. El tramo protegido es la **critical section**. Bien hecho, esto convierte un todos-contra-todos caótico sobre datos compartidos en una cola ordenada de uno en uno, y la race condition desaparece.

_Diagram: El thread A tiene el lock y trabaja en la critical section. El thread B espera fuera._

#### Lock / Mutex (exclusión mutua)

- **Problema:** Dos tareas cambian datos compartidos a la vez — una race.
- **Idea:** Adquirir el lock → trabajar en la **critical section** → soltarlo. Todos los demás esperan.
- **Ejemplo:** Un thread coge el lock, suma uno al contador y lo suelta.
- **Coste:** Las demás tareas esperan; riesgo de **Deadlock** y **Contention**.
- **Cuándo usarlo:** Cuando el estado compartido mutable de verdad no se puede evitar.
- **Relaciones:** Controla **Shared Mutable State**. A menudo más barato: **Atomic Operation** o **Message Passing**.

### Deadlock — todos esperando para siempre

> Dos personas en una puerta estrecha, cada una negándose a retroceder hasta que lo haga la otra — las dos atascadas, con educación, para siempre.

Los locks traen su propio fallo clásico: el **deadlock**. La tarea A tiene el lock 1 y quiere el 2; la tarea B tiene el lock 2 y quiere el 1. Cada una espera a que la otra suelte, y ninguna lo hace jamás — las dos congeladas para siempre. Nace de adquirir varios locks en distinto orden. La cura habitual es la disciplina: adquirir siempre los locks en el mismo orden acordado, para que no pueda formarse la **circular wait**.

_Diagram: A tiene el 1 y quiere el 2, B tiene el 2 y quiere el 1 — el círculo se cierra y las dos esperan para siempre._

#### Deadlock (espera circular)

- **Qué es:** Las tareas se esperan unas a otras en círculo, y ninguna puede continuar.
- **Ejemplo:** A tiene el lock 1 y quiere el 2; B tiene el 2 y quiere el 1.
- **Causa:** Varios locks adquiridos en distinto orden.
- **Remedio:** Coger siempre los locks en el mismo orden acordado.
- **Relaciones:** El peligro clásico de **Lock / Mutex**.

### Lock Contention — los locks son correctos pero costosos

> Una llave de baño mantiene el orden, pero si todos la necesitan constantemente, se forma una cola y toda la oficina va a la velocidad de esa única puerta.

Los locks arreglan las races, pero tienen un precio: mientras una tarea tiene el lock, las demás esperan, así que bloquear mucho puede borrar la velocidad que querías de concurrency en primer lugar. El exceso de locks crea **contention** — todos haciendo cola en el mismo lock. Por eso mantienes los locks el menor tiempo posible, proteges lo mínimo posible y prefieres diseños que necesiten menos locks. Un programa correcto serializado en un solo lock no es mucho mejor que uno de un solo thread.

_Diagram: Cuatro threads hacen cola para un lock — el trabajo pasa de uno en uno._

#### Lock Contention (cola en un lock)

- **Qué es:** Muchas tareas hacen cola para el mismo lock.
- **Ejemplo:** Un programa serializado en un solo lock es casi de un solo thread.
- **Coste:** Borra la ganancia de concurrency: correcto, pero lento.
- **Remedio:** Mantener los locks poco tiempo, proteger lo mínimo, preferir diseños que necesiten menos locks.
- **Relaciones:** El segundo coste de **Lock / Mutex** después de **Deadlock**.

> **La pregunta del lock:** «¿Es esta critical section lo más pequeña posible, estoy adquiriendo los locks en un orden coherente para evitar el deadlock — y la contention está borrando en silencio mis ganancias de concurrency?»

> Un lock da acceso exclusivo a una tarea cada vez, matando la race. Pero los locks pueden provocar deadlock, y bloquear en exceso serializa todo — correcto, pero lento.

## 6. Patrones más seguros

Los locks combaten los síntomas del estado compartido mutable. La jugada más profunda es diseñar para que haya menos que combatir — y unos pocos patrones hacen imposibles clases enteras de errores de concurrency.

### Message Passing — no compartas memoria, pasa mensajes

> En vez de muchos cocineros peleándose por un contador compartido, cada uno trabaja en su puesto y pasa los platos terminados por el pase — nunca dos echan mano de lo mismo.

Una alternativa potente a la memoria compartida es el **message passing**: las tareas no tocan los mismos datos; se envían mensajes a través de una **cola**. Solo una tarea es dueña de cada dato, y las demás piden cambios enviando un mensaje. Hay un lema famoso para esto: «no te comuniques compartiendo memoria; comparte memoria comunicándote». Quita el acceso compartido y quitas la race de raíz, sin necesidad de locks.

_Diagram: Las tareas envían mensajes a una cola. Solo la tarea dueña toca los datos._

#### Message Passing (comunica, no compartas)

- **Problema:** La memoria compartida necesita locks y cría races.
- **Idea:** Una tarea es dueña de los datos; las demás piden cambios enviando mensajes por una cola.
- **Ejemplo:** Cocineros en sus puestos pasan los platos por el pase en vez de compartir un contador.
- **Coste:** Los mensajes son más lentos que el acceso directo a memoria.
- **Cuándo usarlo:** Cuando cada dato puede tener un único dueño.
- **Relaciones:** Quita lo «compartido» de **Shared Mutable State**. También es como hablan los **Processes**.

### Immutability — los datos inmutables no pueden sufrir races

> Dar a cada uno su propia fotocopia en vez de un original compartido — todos pueden leer a la vez, y nadie puede garabatear sobre lo que otro está leyendo.

Si los datos nunca cambian después de crearse — si son **immutable** — entonces cualquier número de tareas puede leerlos a la vez sin ningún riesgo, porque el peligro nunca fue compartir, sino compartir _y cambiar_. En vez de mutar un valor en el sitio, creas uno nuevo. Immutability quita una mitad de la condición «compartido y mutable», y con ella toda una categoría de errores. Por eso los estilos funcionales se apoyan tanto en ella.

_Diagram: El valor v1 nunca cambia — cualquier número de tareas puede leerlo. Una actualización crea un valor nuevo, v2._

#### Immutability (datos inmutables)

- **Problema:** Alguien cambia datos compartidos mientras otros los leen.
- **Idea:** No cambiar un valor en el sitio — crear uno nuevo.
- **Ejemplo:** Cada uno recibe su propia fotocopia; nadie puede garabatear sobre la de otro.
- **Coste:** Copias nuevas en vez de ediciones en el sitio.
- **Cuándo usarlo:** Datos que leen muchas tareas. Un pilar del estilo funcional.
- **Relaciones:** Quita lo «mutable» de **Shared Mutable State**.

### Atomic Operation — actualizaciones indivisibles

> Un torniquete que cuenta a cada persona al pasar en un único clic indivisible — no hay un instante a medio contar por el que dos personas puedan colarse.

Algunas operaciones son **atomic**: ocurren como un único paso indivisible que no se puede interrumpir a medias, así que dos tareas no pueden sorprenderse una a otra en mitad de una actualización. Los lenguajes y las bases de datos ofrecen **atomic counters**, **atomic swaps** y **transactions** justo para las pequeñas actualizaciones compartidas que tanto gustan a las races. Cuando de verdad debes compartir un valor cambiante, una operación atomic suele ser una solución más barata y segura que un lock completo.

_Diagram: «Leer, sumar, escribir» son tres pasos, y otra tarea puede colarse en medio. Un incremento atomic es un solo paso._

#### Atomic Operation (paso indivisible)

- **Problema:** Una actualización de varios pasos puede interrumpirse a medias.
- **Idea:** Hacer de la actualización un único paso indivisible.
- **Ejemplo:** Un atomic counter, un atomic swap, una transaction de base de datos.
- **Coste:** Solo sirve para actualizaciones pequeñas, no para grandes trozos de lógica.
- **Cuándo usarlo:** Un valor compartido que cambia, como un contador.
- **Relaciones:** A menudo más barato y seguro que **Lock / Mutex**; arregla **Race Condition**.

> **La pregunta del patrón:** «¿Puedo evitar del todo el estado compartido mutable aquí — pasando un mensaje, haciendo los datos inmutables o usando una operación atomic — en vez de recurrir a un lock?»

> El mejor error de concurrency es el que se hace imposible: pasa mensajes en vez de compartir, haz los datos inmutables o usa operaciones atomic — y la race no tiene dónde vivir.

## 7. Usarlo bien

Concurrency es un poder que debes añadir a propósito, no por reflejo. La habilidad está en recurrir a ella solo cuando el problema lo pide, y en elegir el modelo más simple que haga el trabajo.

### Adapta la herramienta al cuello de botella

> No contratas más levantadores para un trabajo que es todo espera, ni un mejor planificador para un trabajo que es todo levantar peso — adaptas la ayuda a donde realmente se va el tiempo.

Antes de añadir concurrency, conoce tu **cuello de botella**. Si el trabajo es **I/O-bound** — sobre todo espera — async en un solo thread suele resolverlo con sencillez. Si es **CPU-bound** — sobre todo cálculo — necesitas **parallelism** real entre cores. Si no es ninguno — rápido y simple — añadir concurrency solo te compra errores sin ganancia. El error más común es recurrir a threads cuando el código secuencial normal ya era lo bastante rápido.

_Diagram: Primero pregunta adónde se va el tiempo. Espera → async, cálculo → parallelism, ninguno → déjalo como está._

### Mantén diminuta la superficie compartida

> Un taller donde cada uno trabaja sobre todo solo, con un pequeño soporte de herramientas compartido y bien marcado — cuanto menos se comparte, menos puede salir mal.

Como todo error de concurrency viene del estado compartido mutable, la jugada maestra es **minimizar lo compartido**. Mantén la mayoría de los datos en manos de una sola tarea, haz inmutable lo que puedas y confina las partes realmente compartidas y cambiantes a una superficie pequeña y bien vigilada. Un diseño con una superficie compartida diminuta es uno sobre el que puedes razonar de verdad; uno donde todo es compartido y mutable es una casa encantada. Aquí la simplicidad no es opcional — es toda la defensa.

### Cómo se conectan los conceptos

> Un árbol genealógico: todos los problemas comparten un antepasado, y todas las curas comparten un objetivo.

Casi todo en este curso gira alrededor de un punto. Los **threads** comparten memoria → eso crea **estado compartido mutable** → que cría **races**. Cuatro curas — **lock**, **message passing**, **immutability**, **atomic** — lo controlan o lo eliminan. Los locks traen sus propios problemas: **deadlock** y **contention**. Aparte queda la elección según la carga de trabajo: espera → **async + event loop**, cálculo → **parallelism**.

_Diagram: Las conexiones principales entre los conceptos de este curso._

#### Antes de añadir concurrency

- **Cuál es el cuello de botella** — ¿I/O-bound (async), CPU-bound (parallel) o ninguno (no lo hagas)?
- **Qué datos son compartidos y mutables**, ¿y podrían tocarlos dos tareas a la vez?
- **Puedo evitar compartir** — ¿message passing, immutability o una operación atomic?
- **Si debo usar un lock**, ¿es la critical section diminuta y el orden de los locks coherente?
- **Podría haber deadlock**, ¿y he descartado la circular wait?
- **Vale la pena la complejidad añadida**, ¿o el código secuencial ya era lo bastante rápido?

#### Señales de que lo manejas bien

- Recurres a **async** para esperar y a **parallelism** para calcular — no por reflejo.
- Sabes señalar exactamente qué datos son **compartidos y mutables**, y son pocos.
- Prefieres **message passing e immutability** a esparcir locks por todas partes.
- Tus locks son **cortos y ordenados**, y has razonado sobre el **deadlock**.
- No añades concurrency donde el código **secuencial** normal ya era lo bastante rápido.

> Concurrency es poder deliberado: async para esperar, parallelism para calcular y una superficie compartida diminuta vigilada a propósito. Todo error se rastrea hasta el estado compartido mutable — así que comparte lo menos que puedas.

## Chuleta: los 19 conceptos

### Lo básico

- **Concurrency** (malabares): Un trabajador hace malabares con tareas alternando. Una forma de estructurar el trabajo.
- **Parallelism** (muchas manos): Muchas tareas en el mismo instante en muchos cores. Una forma de ejecutar el trabajo.
- **CPU Core** (core del procesador): Hace una cosa a la vez; un core da concurrency, muchos dan parallelism.

### Threads y processes

- **Process** (programa aislado): Un programa aislado con su propia memoria. Seguro, pero pesado.
- **Thread** (línea de ejecución): Una línea de ejecución dentro de un process; los threads comparten memoria. Rápido y arriesgado.
- **Context Switch** (coste del cambio): Guardar una tarea, cargar otra. No es gratis.

### La espera

- **Blocking Call** (parar y esperar): Parar y esperar la respuesta, sin hacer nada.
- **Async / Await** (non-blocking): Iniciar lo lento, seguir trabajando, volver cuando esté listo.
- **Event Loop** (planificador de un solo thread): Un thread toma una y otra vez la siguiente tarea lista.
- **I/O-bound vs CPU-bound** (espera vs cálculo): Espera → async. Calcula → parallelism.

### Estado compartido

- **Race Condition** (error de momento): El resultado depende del momento de las tareas sobre datos compartidos.
- **Non-determinism** (intermitente): Misma entrada, distinto resultado. El error rara vez aparece en las pruebas.
- **Shared Mutable State** (compartido + modificable): La fuente de toda race. Quita lo «compartido» o lo «mutable».

### Locks

- **Lock / Mutex** (exclusión mutua): Una tarea a la vez en la critical section.
- **Deadlock** (espera circular): Las tareas se esperan en círculo. Remedio: un solo orden de locks.
- **Lock Contention** (cola en un lock): Una cola en un lock borra la velocidad.

### Patrones más seguros

- **Message Passing** (comunica, no compartas): Los datos tienen un dueño; los demás le envían mensajes.
- **Immutability** (datos inmutables): Los datos nunca cambian — todos pueden leerlos a la vez.
- **Atomic Operation** (paso indivisible): Una actualización en un único paso indivisible.

Fin del curso exprés · 7 capítulos · 19 conceptos

Ahora viene la práctica: toma algo que espera mucho — un script que hace muchas llamadas de red una tras otra — y haz las llamadas async para que se ejecuten juntas. Mira cómo termina en una fracción del tiempo. Luego encuentra un contador o una lista compartidos y razona sobre lo que pasa si dos tareas los tocan a la vez. Las ideas se vuelven concretas en el momento en que sientes la aceleración y detectas la race. Pero mantén una idea por encima de las demás: hacer muchas cosas a la vez es como el software se mantiene rápido, y cada parte difícil se rastrea hasta la misma raíz — dos cosas tocando los mismos datos al mismo tiempo. Controla eso, y habrás domado la concurrency.
