7 - Cómo piensa un programa concurrente

Un programa concurrente no sigue un único camino lineal; debe contemplar que varias tareas avanzan de forma entrelazada y que el orden exacto lo decide el planificador del sistema o el runtime. Entender este modelo mental es clave para escribir código correcto y evitar asumir secuencias que nunca se garantizan. También obliga a pensar en términos de eventos y mensajes, no solo de llamadas y retornos.

Adoptar esta mentalidad ayuda a diseñar APIs seguras, elegir estructuras de datos que toleren interleavings (entrecruzamientos de instrucciones entre tareas) y decidir qué operaciones requieren ser atómicas o aisladas. Sin este cambio de perspectiva, es fácil introducir errores que solo aparecen bajo carga o en hardware distinto.

7. Cómo piensa un Programa Concurrente

A continuación se abordan los conceptos esenciales que determinan el comportamiento de programas con múltiples tareas activas. Cada sección incluye intuiciones prácticas para guiar decisiones de diseño y depuración.

7.1 Flujo de control no determinista

En concurrencia, el orden de ejecución puede variar en cada corrida. Dos hilos que imprimen mensajes podrían alternarse de formas distintas, y ambas son válidas. El código debe tolerar estas permutaciones y proteger los datos que no admiten estados intermedios.

Esta variabilidad implica que las pruebas deben considerar resultados equivalentes, no solo un orden fijo. También motiva diseñar funciones idempotentes (que producen el mismo efecto aunque se ejecuten varias veces) y evitar suposiciones como “esta llamada siempre ocurre después de aquella”.

7.2 Planificación por parte del sistema operativo

El planificador del sistema operativo decide cuándo y dónde se ejecuta cada hilo o proceso. Considera prioridades, afinidad a núcleos y políticas de tiempo compartido. Un programa no puede depender de ser ejecutado de inmediato; debe estar preparado para ser pausado y reanudado en cualquier punto no atómico.

Algunos runtimes cooperativos (por ejemplo, corrutinas) delegan parte de la planificación en el desarrollador, pero el principio se mantiene: cualquier tarea puede ceder el control en puntos de espera o llamados a E/S. Comprender qué fragmentos pueden ser interrumpidos permite decidir dónde colocar sincronización y qué invariantes hay que revalidar al retomar.

7.3 Interleaving: ejecución entrelazada

El trabajo de distintas tareas se intercala en fragmentos pequeños. Esta ejecución entrelazada significa que, aunque parezca paralelo, el CPU puede estar alternando rápidamente entre instrucciones de distintos hilos. Por eso las secciones críticas requieren sincronización: cualquier línea podría ser interrumpida antes de terminar la operación completa.

Visualizar el interleaving ayuda a detectar riesgos: una suma parcial, un iterador a mitad de camino o un buffer sin terminar pueden quedar visibles si otro hilo entra en juego. Modelar estas transiciones como pasos atómicos y puntos de pausa reduce sorpresas.

7.4 Concepto de atomicidad

Una operación es atómica cuando ocurre por completo o no ocurre, sin estados intermedios visibles. Operaciones como incrementar un contador compartido no son atómicas a menos que se usen primitivas específicas o locks. Diseñar código atómico reduce condiciones de carrera y simplifica las invariantes.

La atomicidad no implica necesariamente exclusión global: también puede lograrse con estructuras lock-free que usan instrucciones atómicas del hardware. Identificar el tamaño adecuado de cada sección atómica evita sobre-sincronización y mantiene el rendimiento.

7.5 Tiempo lógico vs tiempo real

El tiempo real es el reloj del mundo; el tiempo lógico es el orden relativo de eventos dentro del sistema. En concurrencia, garantizar un orden lógico (por ejemplo, A sucede antes que B) puede ser más importante que medir segundos. Conceptos como relojes lógicos o marcas de tiempo ayudan a razonar sobre causalidad cuando las tareas se ejecutan en paralelo o distribuidas.

En sistemas distribuidos, distintos nodos tienen relojes físicos ligeramente desalineados; el tiempo lógico permite coordinar secuencias sin requerir sincronización perfecta. Aun en una sola máquina, pensar en eventos “antes de” y “después de” es más fiable que contar milisegundos.

Adoptar esta mentalidad permite diseñar programas robustos: asumir no determinismo, sincronizar lo necesario y razonar sobre el orden lógico en lugar de depender del orden físico. También orienta las pruebas: inyectar demoras, variar prioridades y forzar interleavings ayuda a validar que el diseño resiste ejecuciones reales.