CodeGym /Cursos /C# SELF /Excepciones en Parallel.For...

Excepciones en Parallel.For y Parallel.ForEach

C# SELF
Nivel 61 , Lección 2
Disponible

1. Cómo funcionan las excepciones en Parallel.For y Parallel.ForEach

En un bucle normal for todo es sencillo: si dentro del cuerpo del bucle se lanza una excepción — la ejecución del bucle termina y la excepción sale hacia afuera. En los bucles paralelos no es así. Vamos a verlo.

Todas las excepciones se juntan en una "bolsa"

Cuando en una de las iteraciones de un bucle paralelo (Parallel.For/ForEach) ocurre una excepción, no sale inmediatamente hacia arriba, sino que se empaqueta. El proceso continúa: otras iteraciones o bien terminan su trabajo, o también lanzan excepciones. Resultado: cuando el bucle paralelo termina (o se interrumpe forzosamente), todas las excepciones "lanzadas" se recolectan y se lanzan fuera como un único objeto del tipo AggregateException.

AggregateException es un "contenedor" que internamente guarda la colección de todas las excepciones que ocurrieron durante la ejecución de las iteraciones paralelas. Es útil: siempre obtenemos TODAS las fallas (o al menos todas las que alcanzaron a acumularse antes de que terminasen los hilos principales).

Cómo se ve en la práctica

Ejemplo: procesamiento paralelo donde a veces lanzamos una excepción

using System;
using System.Threading.Tasks;

class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 0, 4, 0, 6, 7, 8 };

        try
        {
            Parallel.ForEach(numbers, number =>
            {
                // Dividimos intencionalmente por el número, ¡a veces es cero!
                // Esto provocará DivideByZeroException
                int result = 100 / number;
                Console.WriteLine($"100 / {number} = {result}");
            });
        }
        catch (AggregateException ex)
        {
            Console.WriteLine("¡Se han detectado errores en el ciclo paralelo!");

            // Recorremos todas las excepciones que ocurrieron
            foreach (var inner in ex.InnerExceptions)
            {
                Console.WriteLine($"Tipo: {inner.GetType().Name} — Mensaje: {inner.Message}");
            }
        }
    }
}

Qué ocurrirá:

  • En la colección principal hay ceros, y dividir por 0 es tabú en matemáticas (y en C#): surgirán DivideByZeroException.
  • El bucle paralelo comienza a procesar. Tan pronto como en alguna iteración haga una división por cero — el bucle no se detendrá de inmediato, sino que continuará las iteraciones que ya habían empezado.
  • Cuando todos los hilos terminen su trabajo (unos con error, otros sin), hacia afuera se lanzará un AggregateException que contiene todas las excepciones ocurridas.

Visualicemos la mecánica de manejo de excepciones

flowchart LR
    A[Hilo 1]
    B[Hilo 2]
    C[Hilo 3]
    D[Hilo 4]
    E[Parallel.ForEach]
    F[Excepción 1]
    G[Excepción 2]
    H[AggregateException]
    subgraph Iteraciones
      A --> F
      B --> G
      C --> E
      D --> E
      F --> H
      G --> H
      E --> H
    end

En el diagrama se ve: distintos hilos pueden encontrarse con distintos errores, y todos ellos al final se "empaquetan" en un único AggregateException.

2. Consideraciones prácticas para el manejo de errores

¿Qué hacer con AggregateException?

Cuando capturas un AggregateException, generalmente hay dos escenarios:

  • Mostrar al usuario (o en el log) todos los errores, para aprender de ellos.
  • Determinar qué error es crítico y cuáles son menores: decidir si considerar toda la operación como fallida o ignorar fallos puntuales.

Patrón típico: manejo usando Handle

try
{
    Parallel.For(0, 10, i =>
    {
        if (i == 3 || i == 7)
            throw new InvalidOperationException($"Error en la iteración {i}");
        Console.WriteLine($"Procesado: {i}");
    });
}
catch (AggregateException ex)
{
    ex.Handle(e =>
    {
        if (e is InvalidOperationException)
        {
            Console.WriteLine("Error capturado: " + e.Message);
            // true = el error se considera manejado
            return true;
        }
        // false = no manejado, se lanzará de nuevo
        return false;
    });
}

Este enfoque permite manejar solo las excepciones que consideras "normales", y dejar que lo demás suba, para no ignorar fallos críticos.

Detalles interesantes (y peligrosos) de la implementación

¿Cuándo se detiene el bucle?
Cuando en una iteración ocurre una excepción, Parallel.For/ForEach no inicia nuevas iteraciones, pero las que ya empezaron siguen ejecutándose. Tras finalizar todas las iteraciones activas se lanza el AggregateException. Si hay muchos hilos, la "cola" de trabajo igual se completará — por eso puede haber varias excepciones.

Si no capturas la excepción, la aplicación caerá.
Si no envuelves Parallel.For/ForEach en un bloque try-catch, la aplicación terminará de forma abrupta en la primera excepción encontrada después de que finalicen las iteraciones — no es muy amable con el usuario.

Propagar la excepción "dentro" del bucle.
A veces necesitas un enfoque distinto. Por ejemplo, si quieres que las iteraciones individuales no arruinen todo, puedes manejar las excepciones dentro del cuerpo del bucle paralelo:

Parallel.ForEach(numbers, number =>
{
    try
    {
        int result = 100 / number;
        Console.WriteLine(result);
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Error en el número {number}: {ex.Message}");
    }
});

Este método es bueno si no necesitas todas las excepciones "en bloque": manejas cada fallo in situ (por ejemplo, escribes al log). Pero ojo: si haces esto, no se generará ninguna AggregateException, y no podrás saber si todo fue correcto globalmente.

Si se llama a Break() o Stop().
Si una iteración invoca ParallelLoopState.Break() o ParallelLoopState.Stop(), el bucle intenta detener nuevas iteraciones: Break() finaliza las iteraciones después del índice actual, y Stop() — todas las iteraciones. Sin embargo, si al mismo tiempo ocurre una excepción, ésta se guarda y se lanzará como AggregateException después de que terminen todas las iteraciones activas.

3. Puntos útiles

Excepciones en bucles normales vs paralelos

En un bucle normal cualquier error provoca la terminación inmediata del trabajo: la excepción se lanza hacia afuera y todo queda bloqueado.

En los bucles paralelos C# adopta un enfoque más compensado: el trabajo continúa para las tareas ya iniciadas, y solo al finalizar todo el proceso todas las fallas "salen" juntas. Esto permite recolectar todos los errores sin perder ninguno y decidir tras terminar el bucle qué hacer.

4. Errores típicos al trabajar con excepciones en Parallel.For y Parallel.ForEach

Error #1: ignorar AggregateException.
Si no capturas AggregateException, la aplicación caerá tras terminar todas las iteraciones, provocando pérdida de datos y fallos en servicios o aplicaciones GUI.

Error #2: usar .Wait() sin try-catch.
Llamar .Wait() para Parallel.For/ForEach sin manejar AggregateException llevará a una excepción no controlada, lo que complica el diagnóstico.

Error #3: ignorar errores repetitivos.
Errores idénticos múltiples (por ejemplo, dividir por cero) pueden lanzarse debido a datos repetidos. Sin analizar InnerExceptions puedes perder la causa raíz.

Error #4: silenciar todas las excepciones.
Usar catch (Exception) { /* vacío */ } dentro del bucle oculta errores, provocando pérdida de información y bugs "fantasmas".

Comportamiento de errores en distintos bucles

Opción for/foreach normal Parallel.For / ForEach
Excepción manejada Inmediatamente Después de finalizar todas las iteraciones
Formato del error Exception único AggregateException con colección
Otras iteraciones No se ejecutan Las ya iniciadas terminan
Captura de errores dentro del cuerpo
Captura de errores "desde fuera" Sí, vía AggregateException

"Trucos" y preguntas cortas para entrevistas:

  • ¿Qué pasa si no manejas AggregateException?
    La aplicación caerá después de finalizar todas las iteraciones — independientemente de dónde o cuándo ocurrió el error.
  • ¿Se puede saber en qué iteración exacta ocurrió el error?
    Solo si tú mismo incluyes información del índice o de los datos dentro de la excepción.
  • ¿Puede AggregateException estar vacío?
    No, se crea solo si hay al menos una excepción interna. Si no hay errores, no se lanza.
  • ¿Se manejan los errores si los capturas dentro del bucle?
    Sí, pero entonces "afuera" ya no saldrá nada y no se generará AggregateException.

Ahora estás listo no solo para lanzar bucles en varios hilos, sino también para lidiar con sus "accidentes" paralelos con soltura. Y, como siempre — ten cuidado con la concurrencia: le gustan las sorpresas, especialmente si nadie las captura.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION