2.17

Ver en inglés

2.17 Concurrencia y paralelismo

Visión general y motivación

La concurrencia es el arte de estructurar un programa en tareas independientes que pueden avanzar sin esperar unas a otras. El paralelismo, en cambio, es la ejecución simultánea de esas tareas en varios procesadores. La distinción no es pedantería: la concurrencia organiza el código para que una llamada de red lenta no congele el programa entero; el paralelismo acelera un cálculo voluminoso repartiéndolo entre los núcleos. Confundir ambos conceptos lleva a los equipos a añadir hilos en busca de velocidad y a recibir, a cambio, solo errores.

En equipos grandes, este tema importa porque la concurrencia es donde la corrección muere en silencio. Un autor que escribe código monohilo puede razonar línea a línea; pero en el instante en que varios autores comparten memoria entre hilos, el número de intercalaciones posibles explota, y un programa que supera cada prueba puede fallar una vez en un millón bajo carga de producción, no con un fallo estruendoso sino con datos corruptos, solicitudes encoladas y fallos que nadie logra reproducir. Este capítulo construye sobre el enfoque a nivel de código del capítulo 2.16 (ingeniería de rendimiento) y los cimientos computacionales del 2.13, y alimenta los problemas de coordinación del 2.13 (sistemas distribuidos), que es concurrencia entre máquinas con la crueldad añadida de una red poco fiable.

Para las empresas, los errores de concurrencia son errores de caudal. Los servicios de alto tráfico sobreviven o mueren según su capacidad de atender miles de solicitudes simultáneas sin competir sobre un estado compartido, y un contador sin sincronizar puede corromper un balance contable bajo carga. Para el sector público, las apuestas son la corrección y la trazabilidad en sistemas que operan durante décadas y tocan la seguridad, las prestaciones o los registros públicos. Una carrera en un sistema tributario o sanitario no es una molestia: es una respuesta errónea que alguien deberá justificar ante un organismo de fiscalización. En ambos contextos, el objetivo es el mismo: hacer que la vía segura sea la predeterminada, de modo que las muchas personas que tocan el código no tengan que ser expertas en concurrencia cada una por su cuenta.

Principios fundamentales

  • La concurrencia es estructura; el paralelismo es ejecución. Determine cuál necesita realmente antes de recurrir a los hilos.
  • El estado mutable compartido es el enemigo. Casi todo fallo de concurrencia se remonta a dos tareas que tocan los mismos datos cambiables.
  • Preferir la inmutabilidad y el paso de mensajes. Los datos que no pueden cambiar no admiten carreras, y los mensajes superan a la memoria compartida en seguridad.
  • La no determinación es la dificultad central. El fallo que aparece en una de cada mil ejecuciones es el problema en sí, no un caso marginal.
  • Acotar todo. Las colas sin límite, los grupos de hilos sin tope y el trabajo en curso sin control convierten un pico en una caída.
  • Los modelos de alto nivel superan a los bloqueos manuales. Los actores, los canales y la concurrencia estructurada ofrecen un valor seguro predeterminado para muchos autores, y cada bloqueo tiene un coste.
  • Probar los intercalados, no solo el caso feliz. Las pruebas deterministas no detectan un fallo que solo revela un ordenamiento raro.

Recomendaciones

Determinar si se necesita concurrencia o paralelismo

Comience nombrando el problema. Si el servicio dedica la mayor parte de su tiempo a esperar (a bases de datos, llamadas de red o disco), se trata de una carga acotada por E/S, y la concurrencia es la respuesta: structure el código para que, mientras una solicitud espera, otras sigan avanzando. Un solo hilo con async/await, o un grupo pequeño, puede atender miles de solicitudes en espera. En cambio, si el programa es acotado por CPU, moliendo cómputo con poca espera, entonces el paralelismo entre núcleos es lo que compra velocidad, y aquí el techo lo marca la ley de Amdahl (véase el capítulo 2.16): la fracción serial limita la aceleración por muchos núcleos que se añadan. Mida en qué régimen se encuentra antes de diseñar.

Tratar el estado mutable compartido como el enemigo

Casi todo defecto de concurrencia se reduce a la misma forma: dos tareas leen y escriben los mismos datos cambiables sin acordar un orden. Se trata de una condición de carrera, y produce actualizaciones perdidas, objetos escritos a medias y valores que violan invariantes que el código asumía seguros. La defensa más fiable es disponer de menos estado mutable compartido. Asigne a cada tarea sus propios datos, pase copias en lugar de referencias, y confina el estado mutable a un único propietario al que los demás acceden mediante mensajes. Cuando compartir sea genuinamente necesario, haga el compartir explícito y mínimo, para que un revisor pueda ver cada punto donde se toca ese estado.

Preferir la inmutabilidad y el paso de mensajes como valores predeterminados

Los datos más seguros son los que no pueden cambiar. Un objeto inmutable, una vez construido, puede leerse en cualquier número de hilos sin ninguna sincronización, porque no hay nada sobre lo que competir. Haga de la inmutabilidad su valor predeterminado y de la mutabilidad la excepción deliberada. Cuando las tareas deben coordinarse, prefiera el paso de mensajes a la memoria compartida: en lugar de compartir una variable común, que una tarea envíe el valor a la otra, que es la filosofía del proverbio de Go: «No se comunique compartiendo memoria; comparta memoria comunicándose». El paso de mensajes convierte los fallos invisibles y dependientes del orden en flujos de datos explícitos e inspeccionables, y esa claridad casi siempre compensa el coste por mensaje en código que muchas personas mantienen.

Recurrir a modelos de alto nivel antes que a bloqueos manuales

La codificación manual de bloqueos es correcta en principio y desastrosa en la práctica, porque los seres humanos somos malos razonando sobre cada intercalación. Prefiera modelos que hagan de la concurrencia segura el valor predeterminado. El modelo de actores otorga a cada actor un estado privado y un buzón: los actores nunca comparten memoria, solo envían mensajes, y así desaparecen categorías enteras de carreras. Los procesos secuenciales en comunicación (CSP), el modelo detrás de los canales de lenguajes como Go, tienen procesos independientes que pasan valores a través de canales tipados. La concurrencia estructurada ata la vida de las tareas concurrentes a un ámbito léxico, de modo que las tareas no sobreviven al bloque que las generó y los errores se propagan en vez de desaparecer. El async/await permite escribir código concurrente acotado por E/S con una sintaxis secuencial. Cada uno de estos modelos eleva el suelo para el autor medio, que es lo que un equipo grande necesita.

Comprender el modelo de memoria, la atomicidad y la visibilidad

Cuando se comparte memoria, dos propiedades muerden. La atomicidad significa que una operación ocurre de una vez o no ocurre; un incremento plano (x = x + 1) no es atómico, porque lee, suma y escribe en tres pasos que otro hilo puede interrumpir, y así es como los contadores pierden actualizaciones. La visibilidad significa que una escritura de un hilo se hace observable para otro; sin sincronización adecuada, un valor escrito en un núcleo puede permanecer en caché, invisible para otro, y un hilo puede quedar en bucle infinito sobre una bandera que ya estaba fijada. El modelo de memoria del lenguaje define cuándo se vuelven visibles las escrituras y qué ordenamientos el compilador y la CPU pueden reorganizar, por lo que no puede dar por sentado que el código se ejecuta en el orden en que fue escrito. Utilice los tipos atómicos y las primitivas de sincronización del lenguaje en lugar de inventar un esquema sin bloqueos propio.

Utilizar las primitivas de sincronización con deliberación y diseñar para evitar el interbloqueo

Cuando compartir sea inevitable, recurra a la primitiva adecuada y respete su precio. Un bloqueo o mutex (mutua exclusión) permite a un hilo a la vez entrar en una sección crítica, pero serializa el acceso, de modo que un bloqueo contendido se convierte en un cuello de botella que anula el beneficio de muchos núcleos. Un semáforo limita cuántas tareas pueden avanzar a la vez, que es como se acota un grupo de hilos. Las operaciones atómicas ofrecen actualizaciones sin bloqueos para valores sencillos como contadores, más baratas que un bloqueo pero fáciles de mal emplear para cualquier cosa compuesta. Los bloqueos traen tres modos de fallo clásicos. Un interbloqueo es cuando las tareas se esperan unas a otras en un ciclo y ninguna puede avanzar; el caso de manual es el de dos hilos que cada uno retiene un bloqueo y desea el del otro. El bloqueo vivo es cuando las tareas siguen reaccionando unas a otras pero no hacen progreso. La inanición es cuando una tarea nunca obtiene un recurso porque otras la adelantan sin cesar. Las disciplinas que previenen estos fallos son concretas: imponer un orden global de bloqueos, retener los bloqueos brevemente, añadir tiempo de espera para que una tarea atascada falle con estruendo, no invocar código desconocido mientras se retiene un bloqueo, y emplear planificación justa donde la inanición sea un riesgo. Escríbase estas reglas, porque un autor nuevo no puede rediscoverlas del solo código.

Acotar colas, grupos de hilos y trabajo en curso con retroceso de presión

Una cola sin límite es una bomba de relojería. Bajo un pico de tráfico, el trabajo llega más rápido de lo que se drena, la cola crece sin fin, la memoria se llena y el servicio muere de una forma que parece un misterioso fallo de memoria agotada en lugar de la sobrecarga que es. Acótese cada cola, ponga tope a cada grupo de hilos y aplique retroceso de presión: cuando el sistema esté lleno, señale al emisor que baje la velocidad o rechace el trabajo con rapidez en lugar de aceptar trabajo infinito que no puede terminar. Dimensione los grupos según la carga (aproximadamente el número de núcleos para trabajo acotado por CPU, más alto para trabajo acotado por E/S donde los hilos mayormente esperan), y trate el tope como una decisión de capacidad deliberada. Esto conecta con los patrones de resiliencia del capítulo 3.3.

Emplear paralelismo de datos donde el trabajo es paralelizable con naturalidad

Algunos problemas se dividen con limpieza: aplicar la misma operación a cada elemento de un gran conjunto de datos, sin que ningún elemento dependa de otro. Este paralelismo de datos es el más amable, porque hay poco estado compartido sobre lo que competir y la aceleración puede acercarse al número de núcleos, como muestran los pipelines de mapa-reducción, las operaciones paralelas sobre arrays y el código numérico vectorizado. Incluso aquí, respete la ley de Amdahl: el paso de fusión o reducción suele ser serial y limita la ganancia, y la sobrecarga de la división puede dominar en entradas pequeñas. Recurra a él cuando el trabajo por elemento sea sustancial y los elementos sean verdaderamente independientes; en caso contrario, la versión secuencial más sencilla suele ser suficiente en rapidez y mucho más fácil de mantener correcta, un punto que refuerzan las prácticas de construcción del capítulo 2.9.

Probar y depurar código no determinista a propósito

Los fallos de concurrencia son no deterministas, por lo que las pruebas ordinarias, que ejecutan un solo intercalado, los pasan por alto. Ataque el problema deliberadamente con pruebas de estrés y de borrosidad que lancen muchas tareas con temporización aleatoria para agitar ordenamientos raros. Recurra a detectores de carrera y sanitizadores de hilos, herramientas que instrumentan el acceso a la memoria para captar carreras de datos incluso cuando el intercalado defectuoso no ocurrió en esta ejecución. Donde la plataforma lo permita, use simulación determinista o programadores controlados que reproduzcan un intercalado concreto, convirtiendo un heisenbug en un fallo reproducible, y diseñe para que un cuelgue en producción permita capturar el estado de los hilos y la propiedad de los bloqueos, lo que se conecta con la disciplina de depuración del capítulo 2.15. Ante todo, prefiera diseños (inmutabilidad, paso de mensajes, propiedad única) que hagan imposible ciertas categorías de fallos, porque un fallo que no se puede crear es uno que nunca hay que depurar.

Compromisos: ventajas e inconvenientes

EnfoqueVentajasInconvenientes
Memoria compartida con bloqueosRápido por operación; familiarFallos de carrera, interbloqueo y visibilidad; difícil de mantener correcto entre muchos autores
InmutabilidadNo requiere sincronización; lecturas trivialmente segurasCoste de copiado; torpe con estructuras mutables grandes
Paso de mensajes (actores, canales)Flujo de datos explícito; categorías enteras de fallos desaparecenSobrecarga por mensaje; puede ocultar la presión de retroceso si las colas son ilimitadas
Async/awaitConcurrencia económica para trabajo acotado por E/S; código con apariencia secuencialSin paralelismo para trabajo de CPU; bloquear una tarea estorba a las demás
Concurrencia estructuradaVidas claras de las tareas; propagación de errores; sin tareas fugadasMás nuevo, menos disponible en algunos ecosistemas
Paralelismo de datosAceleración casi lineal en trabajo independienteTecho de Amdahl; la sobrecarga domina en entradas pequeñas
Atómicos / sin bloqueosSin contención de bloqueo para valores sencillosExtremadamente fácil de errar de forma sutil; casi imposible de revisar

La tensión central es la seguridad frente a la velocidad cruda, y la resolución es comprar la corrección primero y gastar rendimiento solo donde la medición lo exija. El bloqueo sobre memoria compartida es el más rápido por operación y el más peligroso por línea de código; los modelos de alto nivel cuestan un poco de caudal y a cambio devuelven seguridad y claridad, y para código mantenido por muchas manos esa transacción vale decididamente la pena. Reserve la concurrencia sin bloqueos ajustada a mano para las pocas zonas calientes donde un perfilador (capítulo 2.16) demuestre que la sobrecarga de coordinación importa, y mantenga incluso esas tras una frontera bien probada.

Preguntas para debatir con el equipo

  1. En su servicio más transitado, ¿la carga es acotada por E/S o por CPU, y el diseño de concurrencia lo refleja? Los equipos suelen añadir grupos de hilos a servicios que pasan el 95 % del tiempo esperando a una base de datos, ganando contención pero no caudal, o intentan paralelizar un cálculo cuya fracción serial limita cualquier aceleración. El diseño correcto fluye del régimen: async o un grupo acotado para trabajo de espera, paralelismo real entre núcleos para trabajo de cómputo. Presente un perfil que muestre dónde va realmente el tiempo, no una suposición, y si la mayor parte del tiempo se dedica a calcular, mida la fracción serial y deje que la ley de Amdahl señale el techo. La respuesta condiciona si se recurre a async, a un grupo acotado o al paralelismo de datos.

  2. ¿Cuál es el valor predeterminado del equipo para compartir estado entre tareas, y es seguro por construcción? En un equipo grande, el valor predeterminado pesa más que las excepciones, porque la mayor parte del código lo escriben personas que no son especialistas en concurrencia y que copian el patrón que ya existe. Si el valor predeterminado es objetos mutables compartidos protegidos por bloqueos ad hoc, basta un bloqueo olvidado para que una carrera aparezca meses después en producción. Si el valor predeterminado es inmutabilidad y paso de mensajes, categorías enteras de fallos nunca ocurren, y el lugar raro que realmente necesita memoria compartida destaca para una revisión cuidadosa. Debate qué es lo que un ingeniero nuevo elegiría hoy, si la revisión captaría una escritura no sincronizada, y cómo hacer que la vía segura sea la más fácil.

  3. ¿Cómo se encontraría, reproduciría y corregiría un fallo de concurrencia que aparece una vez en un millón de solicitudes en producción? La respuesta honesta para muchos equipos es que no podrían, porque el fallo desaparece al observarlo y sus pruebas solo ejecutan un intercalado benigno. Debería preocupar, porque esos fallos corrompen datos en silencio y erosionan la confianza. Hable de si se ejecutan detectores de carrera y sanitizadores de hilos en la integración continua, si se hace prueba de estrés con temporización aleatoria, y si la observabilidad en producción capta el estado de hilos y bloqueos en el instante del colapso. Los mejores equipos responden haciendo imposibles la mayoría de esos fallos por su elección de modelo, de modo que los pocos residuales sean raros y contenedos.

  4. ¿Dónde siguen existiendo colas ilimitadas o grupos de hilos sin tope en el sistema, y qué ocurre con ellas ante un pico súbito de diez veces? Esto importa porque el trabajo en curso ilimitado es el fallo que se disfraza de misterioso agotamiento de memoria: el trabajo llega más rápido de lo que se drena, la memoria se llena y el servicio muere pareciendo un fallo de hardware cuando es sobrecarga pura. Las tensiones son reales, porque un tope puesto demasiado bajo rechaza tráfico legítimo y uno demasiado alto aplaza la caída en lugar de prevenirla, por lo que el número es una decisión de capacidad, no una adivinanza. Presente un inventario de cada cola y grupo, su tope actual (o la admisión de que no lo tiene), el comportamiento de retroceso de presión al llenarse, y evidencia de pruebas de carga de cómo degrada el sistema en el límite. Para una flota empresarial, una cola ilimitada puede desencadenar una caída en cascada en toda la flota, y para una plataforma pública que debe mantenerse disponible para los ciudadanos, el rechazo elegante con un error claro es una obligación de servicio, de modo que el tope y su vía de rechazo pertenecen al plan de capacidad y al manual de operaciones, no a la memoria de un solo ingeniero.

  5. ¿Cuál es la política del equipo respecto a los modelos de concurrencia de alto nivel frente a los bloqueos escritos a mano, y dónde se han admitido excepciones? El modelo predeterminado condiciona lo seguro que es el cambio medio, porque la mayoría de los autores no son especialistas y copiarán el patrón que ya existe: actores, canales y concurrencia estructurada elevan el suelo para todos, mientras que el bloqueo manual es correcto en teoría y fuente de interbloqueos en la práctica. La tensión es que los modelos de alto nivel cuestan un poco de sobrecarga por mensaje o por tarea, y un perfilador ocasionalmente demostrará que una vía caliente necesita código sin bloqueos ajustado a mano, de modo que una prohibición absoluta es tan errónea como un libre albedrío. Presente la lista de lugares donde se bajó del valor seguro predeterminado, la evidencia de perfilado que justificó cada uno, y cómo cada excepción está acotada tras una frontera probada y un orden de bloqueo documentado. En una gran empresa, esta política es lo que impide que miles de contribuyentes reinventen un esquema inseguro cada uno por su cuenta, y en un sistema público de larga vida es lo que permite que un revisor años después comprenda por qué se permitió un patrón peligroso y confirme que sigue estando justificado.

  6. Al decidir paralelizar un cálculo, ¿cómo se mide la fracción serial, y quién es responsable de confirmar que la aceleración es real? Los equipos reparten un cálculo entre núcleos y celebran un número que un perfilador nunca confirmaría, porque la ley de Amdahl limita la ganancia al recíproco de la fracción serial por muchos núcleos que se añadan, y la sobrecarga de división y fusión puede borrar el beneficio por completo en entradas pequeñas. La tensión contraria es que el paralelismo añade complejidad real y superficie nueva de carreras, por lo que la pregunta es si la aceleración medida justifica el riesgo de corrección que se asume. Presente un perfil que aísle la porción serial, los tamaños de entrada donde el paralelismo realmente gana, y un benchmark antes-después en hardware representativo en lugar de una estimación esperanzada. Para una empresa que paga por una flota computacional grande, un análisis honesto de la fracción serial se traduce en gasto de hardware ahorrado o desperdiciado, y para un organismo público responsable del coste de un sistema, quien avaló el diseño en paralelo debe poder mostrar la medición que lo justificó ante la auditoría.

Enfoque por sector

Startup. Con un equipo diminuto y sin holgura que perder, compre corrección con estructura, no con un especialista en concurrencia que no puede contratar. Recurra al valor seguro predeterminado que su lenguaje ofrece: async/await para trabajo acotado por E/S, una tarea propietaria o un actor para cualquier estado compartido, y omita el bloqueo ajustado a mano por completo. Una carrera de actualización perdida en un camino de pagos puede hundir antes que una función postergada, de modo que invierta el pequeño extra de código para hacer imposible esa clase de fallo y siga adelante.

Pequeña empresa. No hay nadie cuyo trabajo sea la concurrencia, por lo que favorezca plataformas y servicios gestionados que la resuelvan por usted: una transacción de base de datos, una cola alojada o el modelo de peticiones de un framework superan a hilos que mantenga a mano. Al evaluar una herramienta, trate «¿hace que la concurrencia sea segura por defecto?» como una cuestión de comprar versus construir, y prefiera la opción donde un intercalado incorrecto no pueda corromper en silencio el registro de un cliente. Mantenga el estado mutable compartido fuera de su propio código siempre que un servicio acotado y gestionado pueda sostenerlo en su lugar.

Gran empresa. A través de muchos equipos, el objetivo es un valor predeterminado propio que mantenga seguros a miles de contribuyentes: inmutabilidad y paso de mensajes como norma, modelos de alto nivel sobre bloqueos manuales, colas y grupos acotados con retroceso de presión, y un orden global de bloqueos documentado. Codifique estos principios en estándares de ingeniería, hágalos cumplir con detectores de carrera y pruebas de estrés en la integración continua, y gobierne las excepciones donde un perfilador justificó código sin bloqueos para que cada una quede tras una frontera probada y revisada. Maneje la capacidad de concurrencia como una cuestión flota-ancha con topes de cola y tamaños de grupo atados a la carga medida.

Sector público. La corrección y la trazabilidad en sistemas que operan durante décadas se anteponen al caudal crudo. Exija que cada transición de estado se registre y sea reproducible, para que una carrera sospechada pueda reproducirse y la corrección demostrarse ante un organismo fiscalizador, y conserve vías deterministas libres de IA para decisiones que toquen prestaciones, seguridad o registros públicos. El aprovisionamiento debe exigir que los proveedores divulguen su modelo de concurrencia y la evidencia de cobertura de detectores de carrera y pruebas de estrés, porque una respuesta errónea bajo carga en un sistema público no es una molestia: es algo que un funcionario responsable deberá justificar después.

Ejemplos

Startup. Un pequeño equipo lanza una función de pagos y observa que los saldos de las cuentas se desvían ocasionalmente en unos céntimos bajo carga. La causa es una lectura-modificación-escritura plana sobre un campo de saldo desde manejadores de peticiones concurrentes: una carrera de actualización perdida. En lugar de esparcir bloqueos, sitúan el saldo de cada cuenta tras una única tarea propietaria que procesa débitos y créditos como mensajes, uno a uno. La desviación desaparece, el código se vuelve fácil de razonar, y añaden una prueba de estrés que dispara miles de transferencias concurrentes para proteger la corrección. Un cambio estructural, una categoría entera de fallo jubilada.

Gran empresa. Un servicio de pedidos de alto caudal que maneja decenas de miles de peticiones por segundo sufre picos de latencia periódicos y ocasionales caídas por agotamiento de memoria durante repuntes de tráfico. La investigación revela una cola de trabajo ilimitada tras un grupo de hilos que crece sin límite una vez que la demanda supera la capacidad. El equipo acota la cola, pone tope al grupo según el número de núcleos y añade retroceso de presión que rechaza la carga excedente con rapidez y un error claro. El caudal se vuelve predecible, las caídas cesan, y un bloqueo contendido sobre una caché compartida se sustituye por una estructura sin bloqueos solo después de que un perfilador demuestre que la contención es real. Valores seguros predeterminados para los muchos autores; concurrencia ajustada solo donde se midió.

Sector público. Una plataforma nacional de prestaciones opera durante décadas y debe producir resultados auditables y correctos incluso bajo actualizaciones concurrentes de casos. El equipo elige inmutabilidad y paso de mensajes como valor predeterminado propio, confina cada pieza de estado mutable a un único propietario e impone un orden global de bloqueos donde estos subsisten, todo plasmado en los estándares de ingeniería. Ejecuta sanitizadores de hilos y pruebas de estrés aleatorizadas en la línea, y diseña de modo que cada transición de estado se registre y sea reproducible para la fiscalización, lo que permite reproducir y demostrar la corrección cuando se sospecha de un intercalado raro. La corrección y la trazabilidad se tratan como requisitos de primera clase, no como consideraciones de rendimiento a posteriori.

Caso de negocio: motivaciones, retorno y coste total de propiedad

El retorno de una concurrencia disciplinada se manifiesta como incidencias que nunca ocurren. Un solo fallo de carrera en producción puede corromper datos en miles de registros, y el coste abarca tanto las horas de ingeniería para hallar un fallo que se esconde al observarlo como el mucho mayor de reconciliar datos erróneos, notificar a los usuarios afectados y reconstruir la confianza. Son algunos de los defectos más caros de diagnosticar precisamente por ser no deterministas, de modo que perseguir un heisenbug puede eclipsar el esfuerzo de elegir un modelo seguro desde el principio.

La ganancia también se muestra en caudal y coste. Dimensionar bien la concurrencia permite que un servicio atienda mucha más carga en el mismo hardware, un ahorro recurrente para una flota grande, mientras que el retroceso de presión y las colas acotadas previenen las caídas en cascada que convierten un pico de tráfico en un incidente público. El coste total de propiedad es modesto y en gran medida cultural: se invierte en un estilo propio (inmutabilidad, paso de mensajes, concurrencia estructurada), en herramientas (detectores de carrera, sanitizadores de hilos, arneses de estrés en la integración continua) y en estándares que codifican el orden de bloqueos y los topes. La alternativa es una base de código donde la corrección depende de que cada autor sea experto para siempre, lo que ningún equipo en crecimiento puede sostener. Presente el caso a la dirección en sus unidades: traduzca una carrera evitada en incidencias de corrupción de datos no sufridas, el retroceso de presión en caídas previnidas, y un valor seguro predeterminado en tiempo de incorporación ahorrado.

Antipatrones y trampas

  • Añadir hilos para velocidad en trabajo acotado por E/S. Más hilos en un servicio de espera compran contención, no caudal.
  • Estado mutable compartido por todas partes. Que cualquier hilo muté cualquier objeto convierte la corrección en cuestión de suerte que ningún revisor puede verificar.
  • Colas y grupos ilimitados. Un pico hace crecer la cola hasta que la memoria muere; la caída parece misteriosa pero es sobrecarga pura.
  • Bloqueos ad hoc sin orden global. Bloqueos adquiridos en distintos órdenes a lo largo del códigobase se interbloquean bajo carga.
  • Suponer que el código se ejecuta en orden de escritura. Ignorar el modelo de memoria, de modo que un fallo de visibilidad deja a un hilo girando en un valor obsoleto.
  • Engaño sin bloqueos hecho a mano. Los esquemas sin bloqueos caseros casi siempre erran de forma sutil y son casi imposibles de revisar.
  • Probar solo el intercalado feliz. Las pruebas deterministas pasan mientras el ordenamiento uno-de-un-millón corrompe producción.
  • Invocar código desconocido mientras se retiene un bloqueo. Una devolución de llamada que bloquea o reingresa convierte una sección crítica en un interbloqueo.

Modelo de madurez

  • Nivel 1, Iniciar: La concurrencia es ad hoc y reactiva. Los hilos y los bloqueos se añaden por instinto, el estado mutable compartido está por todas partes, y las colas son ilimitadas. Las condiciones de carrera se manifiestan como incidencias de producción irreproducibles que nadie logra diagnosticar, y no existe herramienta que las detecte.
  • Nivel 2, Desarrollar: Algunos equipos han aprendido prácticas básicas: emplean bloqueos con más cuidado y acotan sus colas más evidentes. Hay una conciencia informal de carreras e interbloqueos, y unos pocos caminos críticos reciben escrutinio extra. La práctica es inconsistente entre equipos, las pruebas siguen siendo mayormente de un solo intercalado, y los patrones seguros viven en individuos en lugar de estar escritos.
  • Nivel 3, Estandarizar: La organización tiene un estilo propio documentado y aplicado en toda la organización: inmutabilidad y paso de mensajes como valores predeterminados, modelos de alto nivel sobre bloqueos manuales, colas y grupos acotados con retroceso de presión, y un orden global de bloqueos documentado. Detectores de carrera y pruebas de estrés se ejecutan en la integración continua, y las decisiones de concurrencia fluyen de si el trabajo es acotado por E/S o por CPU.
  • Nivel 4, Gestionar: La organización mide y controla su postura de concurrencia frente a baselines. Rastrea la cobertura de detectores de carrera y sanitizadores de hilos entre servicios, registra la profundidad de colas, el tiempo de espera en bloqueos, la saturación de grupos y las tasas de rechazo como métricas monitoreadas, y prueba la curva de degradación por carga para que cada tope sea una decisión de capacidad respaldada por datos. Las incidencias de concurrencia se cuentan y tienden, las fracciones seriales de cargas paralelizadas se miden frente a la aceleración realmente obtenida, y las decisiones de avanzar o no con diseños nuevos se apoyan en esa evidencia en lugar de en el instinto.
  • Nivel 5, Orquestar: La concurrencia segura es el camino de menor resistencia para cada autor, y la práctica se mejora continuamente e integra a través de la organización. Categorias enteras de fallos son imposibles por construcción, las zonas calientes se ajustan solo donde el perfilado lo justifica, y la reproducción determinista hace reproducible el fallo residual raro. La corrección y la trazabilidad son propiedades defendidas de forma continua, los topes de capacidad se adaptan a la carga observada, y los estándares evolucionan conforme a la plataforma y la carga cambian.

Ideas para la reflexión

  1. Si hoy auditara su servicio más transitado, ¿cuánto de su estado es compartido y mutable, y cuánto de ese compartir es verdaderamente necesario?
  2. ¿Cuál es la respuesta predeterminada del equipo cuando alguien necesita coordinar dos tareas, y le resultaría preferible la inmutabilidad o el paso de mensajes?
  3. ¿Dónde siguen escondiéndose colas ilimitadas o grupos sin tope en el sistema, y qué les ocurriría ante un pico súbito de diez veces el tráfico?
  4. ¿Sus ejecuciones de integración continua incluyen un detector de carrera o un sanitizador de hilos, y cuándo captó alguno algo por última vez antes de producción?
  5. Para su carga más paralelizada, ¿cuál es la fracción serial, y ¿la ley de Amdahl limita la aceleración que realmente se persigue?
  6. ¿Su equipo podría reproducir a demanda un fallo de intercalado uno-de-un-millón, y qué tomaría llegar a ello?

Ideas clave

  • La concurrencia estructura un programa en tareas independientes; el paralelismo las ejecuta a la vez. Determine cuál se necesita antes de añadir hilos.
  • El estado mutable compartido es la raíz de casi todo fallo de concurrencia; prefiera inmutabilidad y paso de mensajes como valores seguros predeterminados para muchos autores.
  • Recurra a modelos de alto nivel (actores, canales, concurrencia estructurada, async/await) antes que a bloqueos manuales, correctos en teoría y peligrosos en la práctica.
  • Comprenda la atomicidad, la visibilidad y su modelo de memoria; use la primitiva adecuada, retenga los bloqueos brevemente e imponga un orden global para evitar interbloqueo, bloqueo vivo e inanición.
  • Acótese cada cola y grupo y aplique retroceso de presión, para que un pico degenere con gracia en lugar de caer (capítulo 3.3).
  • Pruebe los intercalados a propósito con detectores de carrera, pruebas de estrés y reproducción (capítulo 2.15), y respete la ley de Amdahl al paralelizar (capítulo 2.16).
  • Para las empresas, se trata de caudal e incidencias prevenidas; para el sector público, de corrección y trazabilidad en sistemas de larga vida.

Referencias y lecturas complementarias

  • Brian Goetz et al., Java Concurrency in Practice (atomicidad, visibilidad, el modelo de memoria y la publicación segura).
  • Herb Sutter, «The Free Lunch Is Over» (por qué el software debe abrazar la concurrencia al aplanarse la velocidad del reloj).
  • Leslie Lamport, «Time, Clocks, and the Ordering of Events in a Distributed System» (orden y los cimientos del razonamiento concurrente).
  • C. A. R. Hoare, «Communicating Sequential Processes» (Communications of the ACM, 1978): el modelo CSP detrás de los canales.
  • Carl Hewitt, Peter Bishop y Richard Steiger, «A Universal Modular Actor Formalism for Artificial Intelligence» (el origen del modelo de actores).
  • Edsger W. Dijkstra, «Cooperating Sequential Processes» (semáforos, mutua exclusión y el problema del interbloqueo).
  • Maurice Herlihy y Nir Shavit, The Art of Multiprocessor Programming (bloqueos, atómicos y estructuras de datos sin bloqueos).
  • Nathaniel J. Smith, «Notes on Structured Concurrency, or: Go Statement Considered Harmful» (el caso a favor de la concurrencia estructurada).
  • Martin Kleppmann, Designing Data-Intensive Applications (concurrencia y coherencia donde la memoria se encuentra con los sistemas distribuidos).
  • Gene M. Amdahl, «Validity of the Single Processor Approach to Achieving Large-Scale Computing Capabilities» (1967): el origen de la ley de Amdahl.