Curso exprés · No. 16
Un programa que hace una cosa a la vez es simple y a menudo demasiado lento. Por eso hacemos que el software haga malabares con muchas tareas, o las ejecute en muchos cores a la vez. De ahí viene la velocidad — y de ahí viene también toda una familia de errores extraños, todos con la misma raíz: dos piezas de trabajo tocando los mismos datos en el mismo instante.
19 conceptos · un diagrama para cada uno · chuleta al final
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.
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.
- 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.
- 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).
- 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.
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.
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.
- 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.
- 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.
- 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.
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.
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.
- 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.
- 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.
- 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.
- 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.
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).
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.
- 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.
- 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.
- 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».
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.
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.
- 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.
- 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.
- 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.
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.
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.
- 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.
- 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.
- 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.
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.
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.
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.
- 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?
- 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
- Concurrencymalabares
Un trabajador hace malabares con tareas alternando. Una forma de estructurar el trabajo.
- Parallelismmuchas manos
Muchas tareas en el mismo instante en muchos cores. Una forma de ejecutar el trabajo.
- CPU Corecore del procesador
Hace una cosa a la vez; un core da concurrency, muchos dan parallelism.
Threads y processes
- Processprograma aislado
Un programa aislado con su propia memoria. Seguro, pero pesado.
- Threadlínea de ejecución
Una línea de ejecución dentro de un process; los threads comparten memoria. Rápido y arriesgado.
- Context Switchcoste del cambio
Guardar una tarea, cargar otra. No es gratis.
La espera
- Blocking Callparar y esperar
Parar y esperar la respuesta, sin hacer nada.
- Async / Awaitnon-blocking
Iniciar lo lento, seguir trabajando, volver cuando esté listo.
- Event Loopplanificador de un solo thread
Un thread toma una y otra vez la siguiente tarea lista.
- I/O-bound vs CPU-boundespera vs cálculo
Espera → async. Calcula → parallelism.
Estado compartido
- Race Conditionerror de momento
El resultado depende del momento de las tareas sobre datos compartidos.
- Non-determinismintermitente
Misma entrada, distinto resultado. El error rara vez aparece en las pruebas.
- Shared Mutable Statecompartido + modificable
La fuente de toda race. Quita lo «compartido» o lo «mutable».
Locks
- Lock / Mutexexclusión mutua
Una tarea a la vez en la critical section.
- Deadlockespera circular
Las tareas se esperan en círculo. Remedio: un solo orden de locks.
- Lock Contentioncola en un lock
Una cola en un lock borra la velocidad.
Patrones más seguros
- Message Passingcomunica, no compartas
Los datos tienen un dueño; los demás le envían mensajes.
- Immutabilitydatos inmutables
Los datos nunca cambian — todos pueden leerlos a la vez.
- Atomic Operationpaso indivisible
Una actualización en un único paso indivisible.