1. Multihilo vs paralelismo
Multihilo: mucho, pero no necesariamente a la vez
El multihilo es cuando tu programa tiene varios hilos de ejecución. Cada hilo es como una línea de acciones independiente: uno calcula algo, otro espera la entrada del usuario, un tercero guarda datos en un archivo. En Java creas hilos con la clase Thread, implementas la interfaz Runnable o usas herramientas de alto nivel como ExecutorService (de ellas hablaremos en la próxima lección).
¡PERO! El multihilo no garantiza que tus tareas se ejecuten realmente al mismo tiempo. Todo depende de cuántos núcleos tenga tu procesador. Si solo hay un núcleo, los hilos simplemente «cambian» entre sí muy rápido — tan rápido que a una persona le parece que todo ocurre a la vez. En realidad, el procesador ejecuta solo un hilo en cada instante, y los demás esperan su turno.
Paralelismo: cuando las tareas realmente se ejecutan a la vez
El paralelismo es cuando tu código realmente se ejecuta simultáneamente en varios núcleos del procesador. Si tienes un ordenador moderno con 4, 8, 16 núcleos — puedes acelerar de verdad el procesamiento de tareas grandes dividiéndolas en partes independientes y repartiéndolas entre los núcleos.
Si hacemos una analogía, el multihilo es cuando tienes un solo cocinero que cambia rápidamente entre la preparación de paella, freír filetes y cortar ensalada. El paralelismo es cuando tienes varios cocineros a la vez y cada uno se encarga de su plato.
¿En qué se diferencian en la práctica?
Multihilo — se trata de comodidad y capacidad de respuesta. Usas varios hilos para que el programa no «se cuelgue»: un hilo espera la red, otro dibuja la interfaz, un tercero calcula algo. Todo parece funcionar en paralelo, pero no necesariamente a la vez.
Paralelismo — se trata de velocidad. Aquí, realmente varios núcleos del procesador ejecutan distintas partes de la tarea simultáneamente para obtener el resultado más rápido.
Dicho de otro modo: el multihilo ayuda a organizar el trabajo, y el paralelismo — a acelerarlo.
Importante:
El multihilo es necesario siempre que haya tareas que se puedan hacer de forma independiente.
El paralelismo es necesario cuando quieres acelerar los cálculos distribuyendo realmente el trabajo entre los núcleos.
Ejemplo: procesamiento de un array grande
Imaginemos que tenemos un array de 10 millones de números y queremos calcular la suma de todos sus elementos.
Secuencialmente:
Un hilo recorre todo el array y calcula la suma. Simple y fiable, pero lento.
Multihilo (pero en un solo núcleo):
Divides el array en 4 partes, creas 4 hilos y cada uno calcula su parte. Pero si solo tienes un núcleo, los hilos simplemente trabajarán por turnos — no habrá aceleración, y la sobrecarga del cambio de hilos incluso puede ralentizar el programa.
En paralelo (en varios núcleos):
Divides el array en 4 partes, lanzas 4 hilos y cada hilo realmente trabaja en su propio núcleo. La suma final se recoge a partir de 4 partes. Esto sí que es más rápido — especialmente con grandes volúmenes de datos.
Ahora bien, implementar el procesamiento secuencial del array es muy sencillo; ya has escrito programas de este tipo muchas veces:
// Ejemplo: procesamiento secuencial de un array
int[] arr = new int[10_000_000];
// ... relleno del array ...
long sum = 0;
for (int x : arr) {
sum += x;
}
System.out.println(sum);
Las versiones multihilo y en paralelo son un poco más complejas; las veremos en las próximas lecciones con herramientas modernas.
2. ¿Para qué sirve el paralelismo?
Los procesadores modernos hace tiempo que superaron el límite de un solo núcleo. Incluso tu smartphone probablemente tenga al menos cuatro, y los ordenadores de sobremesa y servidores — ocho, dieciséis, treinta y dos o más. Si una aplicación sabe utilizar todos esos núcleos, puede funcionar varias veces más rápido.
Antes, el rendimiento de los procesadores crecía aumentando la frecuencia de reloj — hasta aproximadamente mediados de los 2000 esto funcionó de verdad. Pero el aumento de frecuencia chocó con límites físicos, y entonces empezó una nueva era — sistemas multiprocesador y multinúcleo. Ahora triunfan los programas que saben distribuir el trabajo eficazmente entre los núcleos.
¿Dónde acelera de verdad el paralelismo?
- Procesamiento de grandes volúmenes de datos: análisis de logs, estadísticas, agregación — todo lo que se pueda dividir en piezas independientes.
- Renderizado, procesamiento de imágenes y vídeo: cada píxel o fragmento puede procesarse por separado.
- Cálculos científicos, modelado: problemas matemáticos, simulaciones, entrenamiento de modelos.
- Aplicaciones de servidor: atención simultánea de muchos clientes.
- Aplicaciones reactivas: cuando hay que reaccionar rápido a muchos eventos sin bloquear el hilo principal.
¿Cuándo no ayuda el paralelismo?
- Si la tarea es pequeña, la sobrecarga de iniciar el paralelismo puede ser mayor que la ganancia.
- Si la tarea no puede dividirse en partes independientes (por ejemplo, cuando cada paso depende del anterior).
- Cuando hay muchos recursos compartidos (por ejemplo, el mismo archivo) y los hilos empiezan a estorbarse entre sí.
3. Tareas típicas para el paralelismo
Veamos qué tareas se «reparten» con más frecuencia entre núcleos.
Cálculos masivos
- Suma, búsqueda de máximo/mínimo, cálculo de estadísticas sobre un array grande.
- Ejemplo: calcular el valor medio de temperatura de un millón de sensores.
Procesamiento de colecciones
- Filtrado, ordenación y transformación de listas grandes (por ejemplo, procesamiento de pedidos de una tienda online).
- Ejemplo: seleccionar todos los pedidos de más de 10 000 rublos y ordenarlos por fecha.
Renderizado y procesamiento gráfico
- Aplicar un filtro a todos los píxeles de una imagen (por ejemplo, ponerla en blanco y negro).
- Cada píxel se puede procesar de forma independiente — un caso ideal para el paralelismo.
Análisis de datos, big data
- MapReduce, agregación, cálculo de estadísticas sobre volúmenes enormes de datos.
- Ejemplo: procesar los logs de un año para encontrar anomalías.
Ejemplo: suma en paralelo
Supongamos que tenemos un array de 1 millón de números. Se puede dividir en 4 partes y calcular la suma de cada parte en un hilo separado, para después sumar los resultados.
4. Problemas y desafíos del paralelismo
Dificultad de depuración
Cuando el código funciona en varios hilos, los fallos pueden manifestarse solo en casos raros, cuando los hilos se «cruzan» de una forma concreta. A veces el error aparece una de cada 1000 ejecuciones — y es muy difícil de atrapar.
Condiciones de carrera (race condition)
Si varios hilos modifican al mismo tiempo la misma variable u objeto — pueden darse resultados incorrectos. Por ejemplo, dos hilos incrementan a la vez un contador y el valor final resulta menor de lo esperado.
Sincronización
Para evitar las condiciones de carrera, hay que sincronizar el acceso a los datos compartidos — mediante la palabra clave synchronized, bloqueos, variables atómicas y otras herramientas. Esto complica el código y puede acarrear otros problemas (por ejemplo, deadlock — interbloqueo de hilos).
Equilibrio de carga
Si dividiste la tarea en 4 partes y una resulta mucho más pesada que las demás — tres hilos ya han terminado y están ociosos, y el cuarto sigue trabajando. Al final, no hay aceleración.
Sobrecoste
Lanzar hilos, cambiar entre ellos, sincronizar — todo eso requiere tiempo. Si la tarea es pequeña, el paralelismo solo ralentizará la ejecución.
Tabla: comparación de enfoques
| Enfoque | Cuándo es rápido | Cuándo se ralentiza | Ejemplo de uso |
|---|---|---|---|
| Secuencial (1 hilo) | Tareas pequeñas, lógica simple | Grandes volúmenes de datos | Procesar 10 líneas |
| Multihilo (en 1 núcleo) | Tareas asíncronas (espera de IO) | Tareas CPU-bound (limitadas por la CPU) en 1 núcleo | Descarga simultánea de archivos |
| Paralelismo (muchos núcleos) | Tareas grandes e independientes | Tareas pequeñas, fuerte dependencia | Procesamiento de un array grande |
Visualización: cómo se ve
// Procesamiento secuencial (1 hilo)
[Tarea 1][Tarea 2][Tarea 3][Tarea 4]
// Multihilo en un solo núcleo (lógica de cambio)
[Tarea 1] [Tarea 2] [Tarea 3] [Tarea 4]
(pero en realidad solo se ejecuta una a la vez, las demás esperan)
// Paralelismo en cuatro núcleos
[Tarea 1] [Tarea 2] [Tarea 3] [Tarea 4]
(todas se ejecutan a la vez)
5. Errores típicos al intentar paralelizar
Error n.º 1: Paralelizar todo indiscriminadamente. Muchos principiantes piensan: «Cuantos más hilos — ¡más rápido!». En realidad no es así. Si hay pocas tareas o son demasiado simples — no hay ganancia e incluso a veces el programa funciona más lento.
Error n.º 2: Ignorar la sincronización. Si varios hilos trabajan con los mismos datos sin sincronización — aparecerán condiciones de carrera, se romperá la lógica y surgirán bugs difíciles de atrapar.
Error n.º 3: Paralelismo por el paralelismo. El paralelismo no es un fin en sí mismo. Se necesita cuando hay tareas reales que pueden dividirse eficazmente en partes independientes.
Error n.º 4: No tener en cuenta las particularidades de la tarea. Algunas tareas no se pueden paralelizar en absoluto (por ejemplo, cuando el paso N+1 depende del resultado del paso N). En tales casos el paralelismo no aporta ventaja.
Error n.º 5: No tener en cuenta el sobrecoste. Lanzar hilos, cambiar entre ellos, recopilar resultados — todo eso lleva tiempo. Para tareas pequeñas, ese tiempo puede ser mayor que el de la propia ejecución.
GO TO FULL VERSION