Diseñar sistemas concurrentes y paralelos trae ventajas, pero también riesgos que pueden degradar el rendimiento o romper la corrección. Reconocerlos temprano permite mitigarlos con mejores diseños, pruebas y monitoreo.
Estos son los problemas más comunes que aparecen al coordinar tareas y compartir recursos.
Ocurren cuando el resultado depende del orden de ejecución de tareas concurrentes. Si dos hilos leen y escriben la misma variable sin sincronización, el estado final puede ser incorrecto o no determinista. También aparecen en flujos asíncronos (eventos, callbacks) cuando se asume un orden que el planificador no garantiza.
Mitigar exige identificar secciones críticas, usar exclusión mutua o estructuras inmutables, y minimizar el estado compartido. Pruebas con alta concurrencia y revisiones de código enfocadas en accesos a memoria ayudan a detectarlas.
El acceso concurrente sin reglas claras puede dejar estructuras en estados parciales: por ejemplo, eliminar un nodo de una lista enlazada sin coordinar con lectores. Las invariantes se rompen y los algoritmos fallan. Bases de datos y caches distribuidas también sufren inconsistencia si se replican datos sin protocolos de consenso.
Se mitiga con transacciones, locks por sección crítica, estructuras lock-free probadas y modelos de consistencia acordes al dominio (fuerte, eventual). Documentar invariantes y proteger sus actualizaciones reduce errores.
Surgen cuando dos o más tareas esperan indefinidamente recursos que la otra mantiene. Por ejemplo, dos hilos toman un lock distinto y esperan el lock del otro. También pueden aparecer en sistemas distribuidos con bloqueos de filas o archivos.
Prevención: definir un orden total de adquisición de recursos, evitar locks anidados, usar timeouts y mecanismos de rollback, o adoptar diseños sin bloqueos largos (actores, colas). Herramientas de detección de deadlocks y perfiles de locks son valiosas en producción.
Sincronizar en exceso puede borrar los beneficios del paralelismo: locks muy grandes, secciones críticas extensas o barreras innecesarias generan contención y espera activa. También ocurre con el exceso de cambios de contexto o cuando se serializa lo que podría ser paralelo.
Para mitigarlo, dividir secciones críticas, usar estructuras concurrentes de granularidad fina, reducir barreras y medir contención con perfiles. A veces conviene simplificar y elegir un modelo con menos sincronización (por ejemplo, paso de mensajes).
En arquitecturas modernas, escribir en memoria no garantiza que otros hilos lean el valor inmediatamente por caches y reordenamientos. Sin barreras de memoria o modelos coherentes, un hilo puede ver datos obsoletos o leer estados imposibles según el orden fuente.
Usar primitivas atómicas, variables volátiles y sincronización adecuada asegura visibilidad y orden. Entender el modelo de memoria del lenguaje y la CPU (por ejemplo, x86 vs ARM) es clave para escribir código seguro.
Errores concurrentes suelen ser intermitentes y dependen del timing, lo que dificulta reproducirlos. Incluso pueden desaparecer al agregar logs (efecto Heisenbug). Herramientas de trazas, perfiles de locks, detectors de data races y pruebas con inyección de latencias ayudan a provocar condiciones límite.
Simplificar el modelo de concurrencia, aislar responsabilidades y escribir pruebas que fuerzan paralelismo (por ejemplo, aumentando hilos y repeticiones) facilita encontrar y corregir fallas.