Compartir a través de


Control de excepciones (Task Parallel Library)

Las excepciones no controladas que se inician mediante el código de usuario que se ejecuta dentro de una tarea se propagan de nuevo al subproceso de unión, excepto en determinados escenarios que se describen posteriormente en este tema. Las excepciones se propagan cuando se usa uno de los métodos estáticos o de instancia Task.Wait o Task<TResult>.Wait, y estos métodos se controlan si la llamada se enmarca en una instrucción try-catch. Si una tarea es la tarea primaria de unas tareas secundarias asociadas o si se esperan varias tareas, pueden producirse varias excepciones. Para propagar todas las excepciones de nuevo al subproceso que realiza la llamada, la infraestructura de la tarea las encapsula en una instancia de AggregateException. AggregateException tiene una propiedad InnerExceptions que se puede enumerar para examinar todas las excepciones originales que se generaron y controlar (o no) cada una de ellas de forma individual. Aunque solo se inicie una única excepción, se encapsulará en un objeto AggregateException.

Dim task1 = Task.Factory.StartNew(Sub()
                                      Throw New MyCustomException("I'm bad, but not too bad!")
                                  End Sub)

Try
    task1.Wait()
Catch ae As AggregateException
    ' Assume we know what's going on with this particular exception.
    ' Rethrow anything else. AggregateException.Handle provides
    ' another way to express this. See later example.
    For Each ex In ae.InnerExceptions
        If TypeOf (ex) Is MyCustomException Then
            Console.WriteLine(ex.Message)
        Else
            Throw
        End If
    Next

End Try
var task1 = Task.Factory.StartNew(() =>
{
    throw new MyCustomException("I'm bad, but not too bad!");
});

try
{
    task1.Wait();
}
catch (AggregateException ae)
{
    // Assume we know what's going on with this particular exception.
    // Rethrow anything else. AggregateException.Handle provides
    // another way to express this. See later example.
    foreach (var e in ae.InnerExceptions)
    {
        if (e is MyCustomException)
        {
            Console.WriteLine(e.Message);
        }
        else
        {
            throw;
        }
    }

}

Para evitar una excepción no controlada, basta con detectar el objeto AggregateException y omitir las excepciones internas. Sin embargo, esta operación no resulta recomendable porque es igual que detectar el tipo Exception base en escenarios no paralelos. Si desea detectar una excepción sin realizar acciones concretas que la resuelvan, puede dejar al programa en un estado indeterminado.

Si no espera que ninguna tarea propague la excepción ni tiene acceso a su propiedad Exception, la excepción se escalará conforme a la directiva de excepciones de .NET cuando la tarea se recopile como elemento no utilizado.

Cuando las excepciones pueden propagarse de nuevo al subproceso de unión, es posible que una tarea continúe procesando algunos elementos después de que se haya producido la excepción.

NotaNota

Cuando está habilitada la opción "Solo mi código", en algunos casos, Visual Studio se interrumpe en la línea que produce la excepción y muestra el mensaje de error "Excepción no controlada por el código de usuario". Este error es benigno.Puede presionar F5 para continuar y ver el comportamiento de control de excepciones que se muestra en estos ejemplos.Para evitar que Visual Studio se interrumpa con el primer error, desactive la casilla "Solo mi código" bajo Herramientas, Opciones, Depuración, General.

Tareas secundarias asociadas y objetos AggregateException anidados

Si una tarea tiene una tarea secundaria adjunta que inicia una excepción, esa excepción se encapsula en un objeto AggregateException antes de que se propague a la tarea primaria, que encapsula esa excepción en su propio objeto AggregateException antes de propagarla de nuevo al subproceso que realiza la llamada. En casos como este, la propiedad AggregateException().InnerExceptions del objeto AggregateException que se detecta en el método Task.Wait, Task<TResult>.Wait, WaitAny o WaitAll contiene una o varias instancias de AggregateException, pero no las excepciones originales que produjeron el error. Si desea evitar tener que iterar en los objetos anidados AggregateExceptions, puede usar el método Flatten() para quitar todos los objetos anidados AggregateExceptions de forma que la propiedad AggregateException() InnerExceptions contenga las excepciones originales. En el ejemplo siguiente, las instancias anidadas de AggregateException se reducen y se controlan en un solo bucle.

' task1 will throw an AE inside an AE inside an AE
Dim task1 = Task.Factory.StartNew(Sub()
                                      Dim child1 = Task.Factory.StartNew(Sub()
                                                                             Dim child2 = Task.Factory.StartNew(Sub()
                                                                                                                    Throw New MyCustomException("Attached child2 faulted.")
                                                                                                                End Sub,
                                                                                                                TaskCreationOptions.AttachedToParent)
                                                                         End Sub,
                                                                         TaskCreationOptions.AttachedToParent)
                                      ' Uncomment this line to see the exception rethrown.
                                      ' throw new MyCustomException("Attached child1 faulted.")
                                  End Sub)
Try
    task1.Wait()
Catch ae As AggregateException
    For Each ex In ae.Flatten().InnerExceptions
        If TypeOf (ex) Is MyCustomException Then
            Console.WriteLine(ex.Message)
        Else
            Throw
        End If
    Next
    'or like this:
    '  ae.Flatten().Handle(Function(e)
    '                               Return TypeOf (e) Is MyCustomException
    '                   End Function)
End Try
// task1 will throw an AE inside an AE inside an AE
var task1 = Task.Factory.StartNew(() =>
{
    var child1 = Task.Factory.StartNew(() =>
        {
            var child2 = Task.Factory.StartNew(() =>
            {
                throw new MyCustomException("Attached child2 faulted.");
            },
            TaskCreationOptions.AttachedToParent);

            // Uncomment this line to see the exception rethrown.
            // throw new MyCustomException("Attached child1 faulted.");
        },
        TaskCreationOptions.AttachedToParent);
});

try
{
    task1.Wait();
}
catch (AggregateException ae)
{

    foreach (var e in ae.Flatten().InnerExceptions)
    {
        if (e is MyCustomException)
        {
            // Recover from the exception. Here we just
            // print the message for demonstration purposes.
            Console.WriteLine(e.Message);
        }
        else
        {
            throw;
        }
    }
    // or ...
   // ae.Flatten().Handle((ex) => ex is MyCustomException);

}

Excepciones de tareas secundarias desasociadas

De forma predeterminada, las tareas secundarias están desasociadas cuando se crean. Las excepciones producidas por tareas desasociadas deben controlarse o reiniciarse en la tarea primaria inmediata; no se propagan de nuevo al subproceso que realiza la llamada del mismo modo que las tareas secundarias asociadas. La tarea primaria superior puede reiniciar manualmente una excepción de una tarea desasociada para encapsularla en un objeto AggregateException y propagarla de nuevo al subproceso de unión.

Dim task1 = Task.Factory.StartNew(Sub()
                                      Dim nestedTask1 = Task.Factory.StartNew(Sub()
                                                                                  Throw New MyCustomException("Nested task faulted.")
                                                                              End Sub)
                                      ' Here the exception will be escalated back to joining thread.
                                      ' We could use try/catch here to prevent that.
                                      nestedTask1.Wait()
                                  End Sub)
Try
    task1.Wait()
Catch ae As AggregateException
    For Each ex In ae.Flatten().InnerExceptions
        If TypeOf (ex) Is MyCustomException Then
            ' Recover from the exception. Here we just
            ' print the message for demonstration purposes.
            Console.WriteLine(ex.Message)
        End If
    Next
End Try
var task1 = Task.Factory.StartNew(() =>
{

    var nested1 = Task.Factory.StartNew(() =>
    {
        throw new MyCustomException("Nested task faulted.");
    });

    // Here the exception will be escalated back to joining thread.
    // We could use try/catch here to prevent that.
    nested1.Wait();

});

try
{
    task1.Wait();
}
catch (AggregateException ae)
{

    foreach (var e in ae.Flatten().InnerExceptions)
    {
        if (e is MyCustomException)
        {
            // Recover from the exception. Here we just
            // print the message for demonstration purposes.
            Console.WriteLine(e.Message);
        }
    }
}

Aunque se use una tarea de continuación para observar una excepción en una tarea secundaria, la tarea primaria debe seguir observando la excepción.

Excepciones que indican la cancelación cooperativa

Cuando el código de usuario de una tarea responde a una solicitud de cancelación, el procedimiento correcto es producir una excepción OperationCanceledException que se pasa en el token de cancelación con el que se comunicó la solicitud. Antes de intentar propagar la excepción, la instancia de la tarea compara el token de la excepción con el que recibió durante su creación. Si son iguales, la tarea propaga una excepción TaskCanceledException encapsulada en un elemento AggregateException y puede verse cuando se examinan las excepciones internas. Sin embargo, si el subproceso de unión no está esperando la tarea, no se propagará esta excepción concreta. Para obtener más información, vea Cancelación de tareas.

Dim someCondition As Boolean = True
Dim tokenSource = New CancellationTokenSource()
Dim token = tokenSource.Token

Dim task1 = Task.Factory.StartNew(Sub()
                                      Dim ct As CancellationToken = token
                                      While someCondition = True
                                          ' Do some work...
                                          Thread.SpinWait(500000)
                                          ct.ThrowIfCancellationRequested()
                                      End While
                                  End Sub,
                                  token)
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;

var task1 = Task.Factory.StartNew(() =>
{
    CancellationToken ct = token;
    while (someCondition)
    {
        // Do some work...
        Thread.SpinWait(50000);
        ct.ThrowIfCancellationRequested();
    }
},
token);

// No waiting required.

Usar el método Handle para filtrar excepciones internas

El método Handle() puede usarse para filtrar excepciones que pueden tratarse como "controladas" sin necesidad de usar ninguna otra lógica. En el delegado de usuario que se proporciona a Handle(), se puede examinar el tipo de excepción, su propiedad Message() o cualquier otra información sobre esta excepción que permita determinar si es benigna. Las excepciones en las que el delegado devuelve false se reinician inmediatamente en una nueva instancia de AggregateException después de que Handle() devuelve un valor.

En el siguiente fragmento de código se usa un bucle foreach sobre las excepciones internas.

For Each ex In ae.InnerExceptions
    If TypeOf (ex) Is MyCustomException Then
        Console.WriteLine(ex.Message)
    Else
        Throw
    End If
Next
foreach (var e in ae.InnerExceptions)
{
    if (e is MyCustomException)
    {
        Console.WriteLine(e.Message);
    }
    else
    {
        throw;
    }
}

En el siguiente fragmento de código se muestra el uso del método Handle() con la misma función.

ae.Handle(Function(ex)
              Return TypeOf (ex) Is MyCustomException
          End Function)
ae.Handle((ex) =>
{
    return ex is MyCustomException;
});

Observar excepciones mediante la propiedad Task.Exception

Si una tarea se completa con el estado Faulted, se puede examinar su propiedad Exception para detectar qué excepción concreta produjo el error. Un mecanismo adecuado para observar la propiedad Exception es usar una continuación que se ejecute solo si se produce un error en la tarea anterior, tal y como se muestra en el siguiente ejemplo.

Dim task1 = Task.Factory.StartNew(Sub()
                                      Throw New MyCustomException("task1 faulted.")
                                  End Sub).ContinueWith(Sub(t)
                                                            Console.WriteLine("I have observed a {0}", _
                                                                              t.Exception.InnerException.GetType().Name)
                                                        End Sub,
                                                        TaskContinuationOptions.OnlyOnFaulted)
var task1 = Task.Factory.StartNew(() =>
{
    throw new MyCustomException("Task1 faulted.");
})
.ContinueWith((t) =>
    {
        Console.WriteLine("I have observed a {0}",
            t.Exception.InnerException.GetType().Name);
    },
    TaskContinuationOptions.OnlyOnFaulted);

En una aplicación real, el delegado de continuación podría registrar información detallada sobre la excepción y posiblemente generar nuevas tareas para recuperarse de la excepción.

Evento UnobservedTaskException

En algunos escenarios (por ejemplo, cuando se hospedan complementos que no son de confianza), es posible que se produzcan numerosas excepciones benignas y que resulte demasiado difícil observarlas todas manualmente. En estos casos, se puede proceder a controlar el evento TaskScheduler.UnobservedTaskException. La instancia de System.Threading.Tasks.UnobservedTaskExceptionEventArgs que se pasa al controlador se puede utilizar para evitar que la excepción no observada se propague de nuevo al subproceso de unión.

Vea también

Conceptos

Task Parallel Library