Programación concurrente y paralela: definición, prácticas y métricas

Última actualización: 14 octubre 2025
  • Diferencia esencial: concurrencia diseña y coordina tareas; el paralelismo las ejecuta simultáneamente.
  • Requisitos: la concurrencia puede funcionar en un núcleo; el paralelismo exige múltiples unidades de cómputo.
  • Técnicas y modelos: descomposición, mapeo, paralelismo de datos, grafos de tareas y work-pool.
  • Calidad y rendimiento: sincronización correcta, pruebas específicas y métricas como speedup, eficiencia y Amdahl.

Ilustración de programación concurrente y paralela

Si te mueves en el mundillo del desarrollo, seguro que has oído hablar de concurrencia y paralelismo, pero no siempre se explica con claridad dónde acaba una cosa y empieza la otra. En pocas palabras, ambas técnicas buscan aprovechar mejor los recursos del sistema, aunque persiguen objetivos ligeramente distintos y se implementan con herramientas y criterios diferentes.

Antes de liarnos, conviene remarcar que, aunque en el lenguaje cotidiano se usen como sinónimos, no lo son. De hecho, la concurrencia describe cómo estructuramos y coordinamos tareas que coexisten, mientras que el paralelismo se refiere a ejecutar varias de esas tareas al mismo tiempo sobre hardware capaz de hacerlo. A partir de aquí, vamos a desgranarlo sin prisas y con ejemplos claros.

Definición clara de concurrencia y paralelismo

Cuando hablamos de concurrencia, nos referimos a la habilidad de un sistema para gestionar múltiples trabajos que avanzan de forma solapada. Esto puede suceder en una única CPU y sin ejecutar realmente varias instrucciones al mismo tiempo: el sistema alterna entre tareas (interleaving) para que todas progresen, reduciendo tiempos de espera y mejorando la experiencia de usuario. Es un enfoque de diseño y coordinación del software.

El paralelismo, por su parte, se apoya en la idea de divide y vencerás: partimos un problema grande en subproblemas y procesamos esas piezas simultáneamente en varias unidades de cómputo (núcleos, procesadores o nodos). Aquí sí hay ejecución en paralelo real; por tanto, el rendimiento depende en gran medida del hardware disponible.

En la práctica, ambos conceptos están relacionados: la concurrencia describe la orquestación de tareas, mientras que el paralelismo describe la ejecución simultánea. Por eso a veces se dice que la concurrencia se centra en el diseño del software y el paralelismo en su ejecución. Son dos caras de la misma moneda, pero no intercambiables.

Una precisión importante: a veces verás definiciones que equiparan concurrencia con procesar más de un proceso a la vez. En la realidad, esa simultaneidad suele ser aparente en máquinas de un único núcleo, gracias a cambios rápidos de contexto; la simultaneidad real corresponde al paralelismo y requiere varias unidades de procesamiento.

Conceptos de concurrencia y paralelismo

Diferencias clave y requisitos de hardware

Una diferencia práctica está en los requisitos del sistema. Para lograr paralelismo real hacen falta varias unidades de cómputo: múltiples núcleos, procesadores o máquinas; en cambio, la concurrencia puede implementarse en un único núcleo alternando tareas. Esto impacta directamente en el coste de hardware.

También difiere la finalidad principal: mientras la concurrencia reduce la latencia percibida y mejora la capacidad de respuesta de un sistema al gestionar varias tareas en curso, el paralelismo busca acelerar el tiempo total de ejecución de un trabajo dividiéndolo y ejecutándolo al mismo tiempo.

Otra distinción muy útil: en paralelo, las sub–tareas suelen estar estrechamente relacionadas y su resultado se combina en un paso final. En concurrencia, las tareas pueden ser independientes y no dependen unas de otras; cada una avanza a su ritmo y terminará cuando le toque, sin necesariamente sincronizarse con las demás.

Por último, a efectos de diseño, solemos decir que la concurrencia trata de cómo estructuramos el programa en unidades que coexisten y cómo se comunican, mientras que el paralelismo trata de cómo ejecutar esas unidades al unísono para ganar rendimiento. Puedes tener concurrencia sin paralelismo (un solo núcleo alternando tareas) y paralelismo sin demasiada concurrencia (un cálculo puro de datos dividido y ejecutado en paralelo).

Ejemplos cotidianos para no confundirse

Piensa en una app de descargas de música. El usuario selecciona varias canciones y la aplicación permite bajarlas “a la vez”. Aquí la idea principal es que cada descarga es independiente y no condiciona a las demás; si una va lenta, no frena el resto. Eso es un escenario típico de concurrencia: múltiples tareas coexisten y avanzan, posiblemente alternando CPU en un mismo núcleo.

Ahora imagina un comparador de vuelos. Recibimos los criterios del usuario (fechas, destino, número de paradas) y lanzamos búsquedas simultáneas en varias aerolíneas. En este caso, todas las búsquedas colaboran a un único resultado final: encontrar la mejor oferta. Necesitamos esperar a que todas terminen, recopilar los resultados y combinarlos. Esta es una estrategia clásica de paralelismo basada en dividir el trabajo y sincronizar al final.

Te puede interesar:  Cómo aprender electricidad online y qué beneficios tiene

Una observación clave en paralelismo es el paso de unión de resultados. Después de repartir el problema, debe haber una etapa de combinación que integre las respuestas parciales en una solución final. Sin ese paso, el paralelismo no entrega un producto completo.

En el mundo real, ambos patrones conviven con frecuencia. Por ejemplo, una web puede gestionar de forma concurrente muchas peticiones de usuarios y, dentro de cada una, ejecutar rutinas paralelas que aprovechan múltiples núcleos para acelerar cálculos intensivos.

Modelos de hardware y software: una visión general

Para entender las posibilidades, conviene repasar el panorama del hardware paralelo y el software que lo explota. Respecto al hardware, existen distintos niveles de paralelismo: desde múltiples núcleos en un mismo procesador hasta clústeres de máquinas con memoria distribuida. La jerarquía de Flynn clasifica arquitecturas en categorías como SISD, SIMD, MISD y MIMD.

En el lado del software, nos movemos entre procesos y hilos. Un proceso tiene su propio espacio de direcciones, mientras que varios hilos comparten memoria en el mismo proceso. Elegir uno u otro implica decisiones sobre comunicación, aislamiento y coste de sincronización.

De manera operativa, los hilos son ligeros y rápidos para compartir datos, pero exigen extremar el cuidado en la integridad de memoria. Los procesos, en cambio, ofrecen aislamiento y evitan ciertas clases de corrupción, a cambio de comunicaciones más costosas y mayor consumo de recursos.

Objetivos, ejes y desglose de contenidos fundamentales

Un itinerario formativo sólido en programación concurrente y paralela suele organizarse en grandes bloques. El primero es Comprender motivaciones y tendencias: por qué necesitamos computación paralela, qué separaciones de responsabilidades facilitan el diseño y cómo el rendimiento condiciona las decisiones.

En Hardware paralelo se estudian modelos y jerarquías, incluyendo la taxonomía de Flynn, GPUs y sistemas multinúcleo/numa. El bloque de Software paralelo aborda el concepto de proceso y de hilo, sus APIs y la forma de organizar el espacio de direcciones y la memoria compartida o distribuida.

El segundo eje gira en torno a Diseñar soluciones concurrentes y distribuidas. Esto implica conocer qué diferencia a los algoritmos paralelos de los seriales, y cómo analizamos espacio y tiempo en presencia de concurrencia y comunicación.

También se cubren técnicas de descomposición: recursiva, por datos, exploratoria, especulativa y otras. Sin una buena descomposición, el paralelismo no compensa la sobrecarga. Además, hay que mapear tareas a procesos o hilos, equilibrando la carga y minimizando la interacción.

En este eje entran los modelos de programas paralelos más usados: paralelismo de datos (misma operación sobre múltiples elementos), grafo de tareas (dependencias explícitas) y el patrón work-pool (conjunto de trabajos que los hilos consumen dinámicamente). Estos modelos ayudan a razonar sobre distribución y sincronización.

El tercer bloque se orienta a Resolver problemas típicos de sincronización y coordinación. Aquí se estudian casos emblemáticos como productor–consumidor, filósofos comensales, el problema de los fumadores, la barbería, el de Santa Claus, formar agua y el denominado problema de Modus Hall. Cada uno ilustra peligros como inanición, interbloqueos y condiciones de carrera.

Los siguientes bloques se centran en Construir programas concurrentes con recursos compartidos (por hilos) y Construir programas concurrentes con recursos distribuidos (por procesos). En el primero, se trabaja con interfaces como Pthreads y OpenMP, se entiende el espacio de direcciones común y se aprende a rastrear memoria y actividad de hilos.

En el segundo, se aborda la concurrencia por procesos con memoria distribuida y APIs como MPI. Se estudia entrada y salida en paralelo, así como la comunicación punto a punto y colectiva (broadcast, scatter, gather, all-reduce). También se practica el rastreo de memoria y procesos.

Finalmente, se incluyen bloques de Pruebas de software y de Evaluación y comparación. En pruebas se ven técnicas de caja negra y caja blanca adaptadas a la concurrencia y la distribución. En evaluación se cubren Ley de Amdahl, métricas de speedup, eficiencia y escalabilidad, medición del tiempo de pared y visualización con gráficos.

Te puede interesar:  Educación vial: qué es, por qué importa y cómo aplicarla en ciudades

Técnicas de descomposición: cómo partir el problema

Elegir la forma de dividir el trabajo marca la diferencia entre ganar rendimiento o perderlo. La descomposición por datos asigna subconjuntos del dataset a distintos ejecutores; funciona de maravilla en operaciones homogéneas como aplicar un filtro a píxeles o recorrer listas.

La descomposición recursiva aparece en divide y vencerás: se fragmenta el problema en subproblemas más pequeños hasta un umbral; mergesort o quicksort paralelos son ejemplos clásicos. La descomposición exploratoria asigna ramas de búsqueda o escenarios diferentes a distintos hilos o nodos.

La descomposición especulativa ejecuta caminos alternativos o suposiciones en paralelo, aceptando que algunas computaciones se desaprovechen si no eran necesarias. Es útil cuando predecimos que varias ramas podrían ser válidas pero no sabemos cuál a priori.

Sea cual sea la técnica, hay que pensar en el coste de coordinación: si es demasiado alto, el paralelismo pierde eficacia. Por eso el tamaño de grano (work granularity) es clave: tareas ni demasiado pequeñas ni tan grandes que desequilibren la carga.

Mapeo de tareas y balanceo de carga

Una vez decidida la descomposición, toca asignar tareas a procesos o hilos. El mapeo estático reparte el trabajo de antemano; es sencillo y barato, pero puede desequilibrarse si las tareas son irregulares. El mapeo dinámico (o work stealing) permite ajustar sobre la marcha.

El balanceo de carga busca que todos los ejecutores estén ocupados de forma razonable. Para ello se recurre a colas compartidas o locales con robado de trabajo. Además, hay que reducir la sobrecarga de interacción entre tareas, minimizando comunicaciones y zonas críticas.

En entornos distribuidos, la afinidad de datos y la topología de red influyen en la asignación. Acercar los datos a quien los procesa evita latencias y cuellos de botella. En memoria compartida, conviene evitar el falso compartido (false sharing) para que las cachés trabajen a favor y no en contra.

Modelos de programación paralela

El paralelismo de datos aplica la misma operación a elementos distintos en paralelo; es ideal para GPUs y SIMD. El grafo de tareas define nodos con dependencias, habilitando planificadores que ejecutan lo que ya está listo sin violar el orden.

El patrón work-pool agrupa trabajos en una cola; varios trabajadores los consumen y producen resultados. Su fuerza está en la adaptabilidad: si una tarea tarda más, otras avanzan mientras. Se emplea en pipelines, servidores y motores de búsqueda.

Escoger el modelo no solo afecta al rendimiento: también determina la facilidad para razonar sobre la corrección y la posibilidad de introducir o evitar bloqueos, esperas activas y condiciones de carrera.

Concurrencia por hilos y memoria compartida

En memoria compartida, múltiples hilos acceden al mismo espacio de direcciones. Esto permite pasar datos por referencia sin copias, pero obliga a garantizar la integridad. Las herramientas más comunes son Pthreads y OpenMP: la primera ofrece control fino; la segunda, directivas de alto nivel integradas en el compilador.

El reto es escribir código libre de condiciones de carrera. Debemos proteger regiones críticas con mutex o candados de lectura–escritura, utilizar variables de condición para coordinar y apoyarnos en barreras y operaciones de reducción cuando corresponde.

Además, hay que distinguir entre código reentrante y no reentrante: el reentrante puede ejecutarse en paralelo sin riesgo si no mantiene estado global. Es un requisito para hablar con propiedad de código thread-safe, que resiste la ejecución simultánea sin corromper datos.

Concurrencia por procesos y memoria distribuida

Cuando cada proceso tiene su propia memoria, entran en juego bibliotecas como MPI. Aquí los datos no se comparten: se envían mensajes explícitos. Esto aumenta la robustez frente a errores de memoria compartida, a costa de un mayor trabajo de comunicación.

MPI ofrece comunicaciones punto a punto (send/recv) y colectivas (broadcast, scatter, gather, all-to-all, reducciones). A la hora de diseñar, conviene tener claras las necesidades de E/S en paralelo: un buen patrón de acceso a disco puede cambiar el juego en cargas intensivas de datos.

La escalabilidad horizontal es su terreno natural: si el problema crece, añadimos más nodos. Eso sí, la latencia de red y la sincronización colectiva exigen algoritmos que limiten las esperas y favorezcan la computación local.

Sincronización: mecanismos y trampas frecuentes

Para coordinar hilos o procesos recurrimos a primitivos de sincronización. En memoria compartida, los clásicos son mutex, semáforos, candados de lectura–escritura y variables de condición. A mayor nivel, usamos barreras y reducciones para puntos de encuentro globales.

Te puede interesar:  Cómo quitar el maquillaje de la ropa

La espera activa (busy waiting) tiene mala prensa, y con razón: ocupa CPU sin avanzar trabajo útil. Solo se justifica en escenarios de latencia mínima y duración brevísima. En general preferimos esperas bloqueantes que liberen la CPU hasta que haya trabajo.

Los problemas típicos incluyen deadlocks (bloqueos mutuos), livelocks (nadie progresa aunque todos se mueven), inanición (algún hilo nunca avanza) y prioridades mal gestionadas. Diseñar con orden total en la adquisición de recursos y tiempos de espera razonables ayuda a evitarlos.

Pruebas de correctitud en programas concurrentes

Probar software concurrente no es trivial porque los fallos pueden ser no deterministas. Aun así, hay estrategias. Las pruebas de caja negra validan la interfaz y el comportamiento observable; las de caja blanca fuerzan intercalados concretos para exponer carreras y bloqueos.

Herramientas de detección de data races, análisis estático y model checking son grandes aliadas. Además, es recomendable inyectar fallos (por ejemplo, añadir aleatoriamente pequeñas esperas) para aumentar la probabilidad de revelar interleavings problemáticos durante las pruebas.

En procesos distribuidos, los test de comunicaciones y la simulación de particiones de red son esenciales. Y nunca olvides registrar con precisión: un buen rastreo de eventos y tiempos es oro puro cuando toca depurar.

Métricas de rendimiento: speedup, eficiencia y escalabilidad

Para evaluar de forma objetiva, usamos métricas estándar. El speedup mide cuánto se acelera un programa paralelo respecto al serial. La eficiencia normaliza ese speedup por el número de recursos, indicando qué fracción de la capacidad estamos aprovechando.

La Ley de Amdahl recuerda que el tramo no paralelizable limita el speedup máximo: aunque añadas infinitos recursos, la parte secuencial pone techo. En paralelo, la Ley de Gustafson ofrece otra perspectiva cuando el tamaño del problema crece con los recursos.

La escalabilidad describe cómo varía el rendimiento al aumentar datos y recursos. Distinguimos entre escalabilidad fuerte (problema fijo, más recursos) y débil (crece el problema a la par que los recursos). Ambas exigen medir con rigor el tiempo de pared (wall-clock) y presentar los resultados en gráficos claros.

Requisitos prácticos y costes

En paralelismo, el hardware manda: para ejecutar de verdad varias tareas a la vez hacen falta varios núcleos o máquinas. Eso trae costes y complejidad operativa. En concurrencia, en cambio, puedes mejorar la capacidad de respuesta en un único núcleo, con menos inversión, si diseñas bien.

Que no te engañen: no todo paralelismo acelera. Si la comunicación o la sincronización dominan, la sobrecarga puede comerse el beneficio. Por eso insistimos tanto en descomposición, mapeo y elección del modelo adecuado para cada problema.

Casos clásicos de estudio y patrones

Los ejercicios canónicos existen por una razón: enseñan patrones de coordinación y las trampas a evitar. Productor–consumidor ilustra colas, señales y backpressure. Filósofos comensales expone deadlocks y cómo romper simetrías para evitarlos.

El problema de los fumadores, la barbería y el de Santa Claus obligan a pensar en ordenaciones, notificaciones y equidad. Formar agua enseña combinaciones atómicas y barreras para alinear entidades. Modus Hall (en su formulación clásica) es otra excusa para practicar estos mecanismos.

Combinar estos patrones con buenas bibliotecas (Pthreads, OpenMP, MPI) y pruebas sólidas te prepara para la vida real: servicios web de alta concurrencia, cómputo científico que exprime los núcleos y pipelines de datos distribuidos.

Cuando todo esto se aplica con cabeza, el resultado es software más rápido, más escalable y más fluido de usar. La clave está en saber cuándo te conviene concurrencia, cuándo paralelismo y, sobre todo, cómo hacerlos convivir sin pisarse en una arquitectura bien pensada.

Mirando el conjunto, la concurrencia te permite servir más tareas a la vez y responder con agilidad, incluso con un único núcleo mediante alternancia, mientras que el paralelismo potencia el rendimiento resolviendo una tarea única de forma simultánea en varias unidades de cómputo; con técnicas de descomposición, mapeo, sincronización y pruebas adecuadas, y apoyándose en modelos como paralelismo de datos, grafos de tareas o work-pool, es posible diseñar sistemas que gestionen mucha carga y, además, corran más rápido sin perder correctitud.