fedorthinks

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

§ 01

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.

BasicsConcurrencyParallelismCPU coreThreads & processesProcessThreadContext switchWaitingBlocking callAsync / awaitEvent loopI/O- vs CPU-boundShared stateRace conditionNon-determinismShared mutable stateLocksLock / mutexDeadlockContentionSafer patternsMessage passingImmutabilityAtomic operation
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.

WorkerABCDACBDABCDtime4 tasks · 1 worker · fast switchingprogress on all four — but never two at the same instant
Un trabajador, cuatro tareas: cambia rápido y todas avanzan — pero solo una se ejecuta en cada instante.
ConceptConcurrency· 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.

Core 1Task ACore 2Task BCore 3Task CCore 4Task Dsame instant4 tasksreally runtogethertime
Cuatro cores, cuatro tareas — en el mismo instante, las cuatro se ejecutan de verdad.
ConceptParallelism· 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).

Core 1ABABABCore 2CDCDCDtimeswitching inside one core = concurrencytwo cores running at once = parallelism
Dentro de cada core, las tareas se turnan (concurrency); los dos cores se ejecutan al mismo tiempo (parallelism).
TermCPU 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.

one at a timeAA waits (network)BB waits (disk)done (late)overlapped (concurrent)AA waitsBB waitsdone (sooner)while A waits, the worker already starts B
De una en una: el trabajador se queda parado mientras espera. Solapadas: mientras A espera, B ya se está ejecutando — y todo termina antes.

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.

§ 02

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.

Process APrivate memoryits own thread(s)Process BPrivate memoryits own thread(s)messagereplynothing shared → safe, but heavier to start and to talk
Cada process tiene su propia memoria. Hablan solo mediante mensajes — es seguro, pero más caro.
ConceptProcess· 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.

One processThread 1Thread 2Thread 3Shared memorythe same data for every threadcheap to coordinate · can collide on the same data
Tres threads de un mismo process miran todos la misma memoria compartida.
ConceptThread· 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.

a few switches — fineTask AswitchTask BswitchTask Athrashing — thousands of tiny tasksAswBswCswDswEswFswGswHswIswJswsave state · load state — past a point the juggling costs more than it saves
Unos pocos cambios no son nada. Miles de tareas diminutas — y el tiempo se va en el cambio mismo.
TermContext 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.

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.

§ 03

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.

Workerasks and standsDatabaseslow answerqueryanswer (much later)workblocked — doing nothingworkthe worker stands still until the answer comes back
El trabajador envía una consulta y se queda parado hasta que vuelve la respuesta.
TermBlocking 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.

Worker (one thread)start Astart Bstart Cother workA ✓C ✓B ✓await A — in flightawait B — in flightawait C — in flightstart the slow call, keep working, pick up the result when readyawait = “come back here when it finishes”
El trabajador inicia A, B y C, hace otro trabajo y recoge cada resultado cuando está listo.
ConceptAsync / 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.

Ready queuereply arrivedtimer firedfile readEvent loopone threadSlow I/Onetwork · disknextstartrepeatwhen done, its callback joins the queue
El loop toma la siguiente tarea lista, lanza el trabajo lento y repite. Las respuestas terminadas se suman a la cola.
ConceptEvent 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.

I/O-boundcpuwaitcpuwaitcpuAsyncjuggle the waitingCPU-boundbusy computingParallelismmore coresasync can't help work that never waits
Izquierda: el trabajo sobre todo espera — async lo salva. Derecha: el core siempre está ocupado — necesitas más cores.
TermI/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.

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).

§ 04

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.

Thread AcounterThread Bread 5read 5write 5 + 1 = 6write 5 + 1 = 65566expected 7 — got 6: one update silently lostno logic mistake — the steps just interleaved badly
Los dos threads leen 5, los dos escriben 6. Esperábamos 7 — una actualización se perdió en silencio.
TermRace 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.

same code · same input · 60 runs✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✗✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓one run fails — it depends on micro-timing, and rarely shows up in tests
Mismo código, misma entrada: casi todas las ejecuciones pasan y una falla — por el momento.
TermNon-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.

not sharedsharedimmutablemutablesafeprivate and frozensaferead freelysafeowned by one taskRACEthe danger zoneremove either property and the race vanishes
Solo una casilla de las cuatro es peligrosa: datos que son a la vez compartidos y mutables.
ConceptShared 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».

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.

§ 05

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.

critical sectionThread Aholds the lockThread BwaitsLockShared datacounterblockedacquire → work → release: one task at a time
El thread A tiene el lock y trabaja en la critical section. El thread B espera fuera.
ToolLock / 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.

Task ALock 1Task BLock 2holdsholdswantswantscircular waitfix: always take locks in the same order (1, then 2)
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.
TermDeadlock· 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.

waiting in lineT5T4T3T2LockT1insideheavy locking serialises the work — correct, but slowhold locks briefly · protect as little as possible
Cuatro threads hacen cola para un lock — el trabajo pasa de uno en uno.
TermLock 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.

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.

§ 06

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.

Task ATask BQueuemsgmsgmsgOwner taskonly it touches the datadon't communicate by sharing memory;share memory by communicating
Las tareas envían mensajes a una cola. Solo la tarea dueña toca los datos.
PatternMessage 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.

Value v1never changesValue v2a new copyupdateReader 1Reader 2Reader 3many readers at once — nothing to race on
El valor v1 nunca cambia — cualquier número de tareas puede leerlo. Una actualización crea un valor nuevo, v2.
PatternImmutability· 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.

not atomic: three stepsreadadd 1writeanother task can slip in between the stepsatomic: one stepincrement — one indivisible stepatomic counters · atomic swaps · transactions
«Leer, sumar, escribir» son tres pasos, y otra tarea puede colarse en medio. Un incremento atomic es un solo paso.
PatternAtomic 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.

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.

§ 07

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.

Where does time go?AsyncI/O-bound: waitingParallelismCPU-bound: computingSequentialalready fast enoughmostly waitingcomputingneithermost common mistake: threads where plain code was enough
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.

Threadsshare memoryShared mutable stateRace conditioncausesLock / mutexMessage passingImmutabilityAtomic opsDeadlockContentionI/O-boundAsync + event loopCPU-boundParallelism (cores)every fix controls, limits or removes shared mutable state
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

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.

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.