Partilhar via


Tratamento de exceções (Biblioteca paralela de tarefas)

As exceções sem tratamento geradas pelo código do usuário em execução dentro de uma tarefa são propagadas de volta para o thread de chamada, exceto em determinados cenários descritos posteriormente neste tópico. As exceções são propagadas quando você usa um dos métodos estáticos ou de instância Task.Wait e as manipula colocando a chamada em uma try/catch instrução. Se uma tarefa for o pai de tarefas filhas anexadas ou se você estiver aguardando em várias tarefas, várias exceções poderão ser lançadas.

Para propagar todas as exceções de volta ao thread de chamada, a infraestrutura de tarefas as encapsula em uma AggregateException instância. A AggregateException exceção tem uma InnerExceptions propriedade que pode ser enumerada para examinar todas as exceções originais que foram lançadas e manipular (ou não) cada uma individualmente. Você também pode manipular as exceções originais usando o AggregateException.Handle método.

Mesmo que apenas uma exceção seja lançada, ela ainda está envolvida em uma AggregateException exceção, como mostra o exemplo a seguir.


public static partial class Program
{
    public static void HandleThree()
    {
        var task = Task.Run(
            () => throw new CustomException("This exception is expected!"));

        try
        {
            task.Wait();
        }
        catch (AggregateException ae)
        {
            foreach (var ex in ae.InnerExceptions)
            {
                // Handle the custom exception.
                if (ex is CustomException)
                {
                    Console.WriteLine(ex.Message);
                }
                // Rethrow any other exception.
                else
                {
                    throw ex;
                }
            }
        }
    }
}
// The example displays the following output:
//        This exception is expected!
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task.Run(Sub() Throw New CustomException("This exception is expected!"))

        Try
            task1.Wait()
        Catch ae As AggregateException
            For Each ex In ae.InnerExceptions
                ' Handle the custom exception.
                If TypeOf ex Is CustomException Then
                    Console.WriteLine(ex.Message)
                    ' Rethrow any other exception.
                Else
                    Throw ex
                End If
            Next
        End Try
    End Sub
End Module

Class CustomException : Inherits Exception
    Public Sub New(s As String)
        MyBase.New(s)
    End Sub
End Class
' The example displays the following output:
'       This exception is expected!

Você poderia evitar uma exceção não tratada apenas pegando e AggregateException não observando nenhuma das exceções internas. No entanto, recomendamos que você não faça isso porque é análogo a capturar o tipo base Exception em cenários não paralelos. Capturar uma exceção sem tomar ações específicas para recuperá-la pode deixar seu programa em um estado indeterminado.

Se você não quiser chamar o método para aguardar a Task.Wait conclusão de uma tarefa, também poderá recuperar a AggregateException exceção da propriedade da tarefa Exception , como mostra o exemplo a seguir. Para obter mais informações, consulte a seção Observando exceções usando a propriedade Task.Exception neste tópico.


public static partial class Program
{
    public static void HandleFour()
    {
        var task = Task.Run(
            () => throw new CustomException("This exception is expected!"));

        while (!task.IsCompleted) { }

        if (task.Status == TaskStatus.Faulted)
        {
            foreach (var ex in task.Exception?.InnerExceptions ?? new(Array.Empty<Exception>()))
            {
                // Handle the custom exception.
                if (ex is CustomException)
                {
                    Console.WriteLine(ex.Message);
                }
                // Rethrow any other exception.
                else
                {
                    throw ex;
                }
            }
        }
    }
}
// The example displays the following output:
//        This exception is expected!
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task.Run(Sub() Throw New CustomException("This exception is expected!"))

        While Not task1.IsCompleted
        End While

        If task1.Status = TaskStatus.Faulted Then
            For Each ex In task1.Exception.InnerExceptions
                ' Handle the custom exception.
                If TypeOf ex Is CustomException Then
                    Console.WriteLine(ex.Message)
                    ' Rethrow any other exception.
                Else
                    Throw ex
                End If
            Next
        End If
    End Sub
End Module

Class CustomException : Inherits Exception
    Public Sub New(s As String)
        MyBase.New(s)
    End Sub
End Class
' The example displays the following output:
'       This exception is expected!

Atenção

O código de exemplo anterior inclui um while loop que sonda a propriedade da Task.IsCompleted tarefa para determinar quando a tarefa foi concluída. Isso nunca deve ser feito no código de produção, pois é muito ineficiente.

Se você não esperar em uma tarefa que propaga uma exceção ou acessar sua Exception propriedade, a exceção será escalada de acordo com a diretiva de exceção do .NET quando a tarefa for coletada de lixo.

Quando as exceções são permitidas para borbulhar de volta para o thread de junção, é possível que uma tarefa continue a processar alguns itens depois que a exceção for gerada.

Nota

Quando "Just My Code" está habilitado, o Visual Studio em alguns casos quebrará na linha que lança a exceção e exibirá uma mensagem de erro que diz "exceção não tratada pelo código do usuário". Este erro é benigno. Você pode pressionar F5 para continuar e ver o comportamento de manipulação de exceções demonstrado nesses exemplos. Para evitar que o Visual Studio quebre no primeiro erro, basta desmarcar a caixa de seleção Habilitar Apenas Meu Código em Ferramentas, Opções, Depuração, Geral.

Tarefas filhas anexadas e AggregateExceptions aninhadas

Se uma tarefa tiver uma tarefa filho anexada que lance uma exceção, essa exceção será encapsulada em um AggregateException antes de ser propagada para a tarefa pai, que encapsula essa exceção em sua própria antes AggregateException de propagá-la de volta para o thread de chamada. Nesses casos, a InnerExceptions propriedade da exceção que é capturada AggregateExceptionWaitAnyTask.Waitno , ou WaitAll método contém uma ou mais AggregateException instâncias, não as exceções originais que causaram a falha. Para evitar ter que iterar sobre exceções aninhadas AggregateException , você pode usar o Flatten método para remover todas as exceções aninhadas AggregateException , para que a AggregateException.InnerExceptions propriedade contenha as exceções originais. No exemplo a seguir, as instâncias aninhadas AggregateException são niveladas e manipuladas em apenas um loop.


public static partial class Program
{
    public static void FlattenTwo()
    {
        var task = Task.Factory.StartNew(() =>
        {
            var child = Task.Factory.StartNew(() =>
            {
                var grandChild = Task.Factory.StartNew(() =>
                {
                    // This exception is nested inside three AggregateExceptions.
                    throw new CustomException("Attached child2 faulted.");
                }, TaskCreationOptions.AttachedToParent);

                // This exception is nested inside two AggregateExceptions.
                throw new CustomException("Attached child1 faulted.");
            }, TaskCreationOptions.AttachedToParent);
        });

        try
        {
            task.Wait();
        }
        catch (AggregateException ae)
        {
            foreach (var ex in ae.Flatten().InnerExceptions)
            {
                if (ex is CustomException)
                {
                    Console.WriteLine(ex.Message);
                }
                else
                {
                    throw;
                }
            }
        }
    }
}
// The example displays the following output:
//    Attached child1 faulted.
//    Attached child2 faulted.
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task.Factory.StartNew(Sub()
                                              Dim child1 = Task.Factory.StartNew(Sub()
                                                                                     Dim child2 = Task.Factory.StartNew(Sub()
                                                                                                                            Throw New CustomException("Attached child2 faulted.")
                                                                                                                        End Sub,
                                                                                                                        TaskCreationOptions.AttachedToParent)
                                                                                     Throw New CustomException("Attached child1 faulted.")
                                                                                 End Sub,
                                                                                 TaskCreationOptions.AttachedToParent)
                                          End Sub)

        Try
            task1.Wait()
        Catch ae As AggregateException
            For Each ex In ae.Flatten().InnerExceptions
                If TypeOf ex Is CustomException Then
                    Console.WriteLine(ex.Message)
                Else
                    Throw
                End If
            Next
        End Try
    End Sub
End Module

Class CustomException : Inherits Exception
    Public Sub New(s As String)
        MyBase.New(s)
    End Sub
End Class
' The example displays the following output:
'       Attached child1 faulted.
'       Attached child2 faulted.

Você também pode usar o AggregateException.Flatten método para relançar as exceções internas de várias AggregateException instâncias lançadas por várias tarefas em uma única AggregateException instância, como mostra o exemplo a seguir.

public static partial class Program
{
    public static void TaskExceptionTwo()
    {
        try
        {
            ExecuteTasks();
        }
        catch (AggregateException ae)
        {
            foreach (var e in ae.InnerExceptions)
            {
                Console.WriteLine(
                    "{0}:\n   {1}", e.GetType().Name, e.Message);
            }
        }
    }

    static void ExecuteTasks()
    {
        // Assume this is a user-entered String.
        string path = @"C:\";
        List<Task> tasks = new();

        tasks.Add(Task.Run(() =>
        {
            // This should throw an UnauthorizedAccessException.
            return Directory.GetFiles(
                path, "*.txt",
                SearchOption.AllDirectories);
        }));

        tasks.Add(Task.Run(() =>
        {
            if (path == @"C:\")
            {
                throw new ArgumentException(
                    "The system root is not a valid path.");
            }
            return new string[] { ".txt", ".dll", ".exe", ".bin", ".dat" };
        }));

        tasks.Add(Task.Run(() =>
        {
            throw new NotImplementedException(
                "This operation has not been implemented.");
        }));

        try
        {
            Task.WaitAll(tasks.ToArray());
        }
        catch (AggregateException ae)
        {
            throw ae.Flatten();
        }
    }
}
// The example displays the following output:
//       UnauthorizedAccessException:
//          Access to the path 'C:\Documents and Settings' is denied.
//       ArgumentException:
//          The system root is not a valid path.
//       NotImplementedException:
//          This operation has not been implemented.
Imports System.Collections.Generic
Imports System.IO
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Try
            ExecuteTasks()
        Catch ae As AggregateException
            For Each e In ae.InnerExceptions
                Console.WriteLine("{0}:{2}   {1}", e.GetType().Name, e.Message,
                                  vbCrLf)
            Next
        End Try
    End Sub

    Sub ExecuteTasks()
        ' Assume this is a user-entered String.
        Dim path = "C:\"
        Dim tasks As New List(Of Task)

        tasks.Add(Task.Run(Function()
                               ' This should throw an UnauthorizedAccessException.
                               Return Directory.GetFiles(path, "*.txt",
                                                         SearchOption.AllDirectories)
                           End Function))

        tasks.Add(Task.Run(Function()
                               If path = "C:\" Then
                                   Throw New ArgumentException("The system root is not a valid path.")
                               End If
                               Return {".txt", ".dll", ".exe", ".bin", ".dat"}
                           End Function))

        tasks.Add(Task.Run(Sub()
                               Throw New NotImplementedException("This operation has not been implemented.")
                           End Sub))

        Try
            Task.WaitAll(tasks.ToArray)
        Catch ae As AggregateException
            Throw ae.Flatten()
        End Try
    End Sub
End Module
' The example displays the following output:
'       UnauthorizedAccessException:
'          Access to the path 'C:\Documents and Settings' is denied.
'       ArgumentException:
'          The system root is not a valid path.
'       NotImplementedException:
'          This operation has not been implemented.

Exceções de tarefas filhas desanexadas

Por padrão, as tarefas filho são criadas como desanexadas. As exceções lançadas de tarefas desanexadas devem ser manipuladas ou relançadas na tarefa pai imediata; eles não são propagados de volta para o thread de chamada da mesma forma que as tarefas filhas anexadas propagadas de volta. O pai mais alto pode relançar manualmente uma exceção de um filho desanexado para fazer com que ela seja encapsulada em um AggregateException e propagada de volta para o thread de chamada.


public static partial class Program
{
    public static void DetachedTwo()
    {
        var task = Task.Run(() =>
        {
            var nestedTask = Task.Run(
                () => throw new CustomException("Detached child task faulted."));

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

        try
        {
            task.Wait();
        }
        catch (AggregateException ae)
        {
            foreach (var e in ae.Flatten().InnerExceptions)
            {
                if (e is CustomException)
                {
                    Console.WriteLine(e.Message);
                }
            }
        }
    }
}
// The example displays the following output:
//    Detached child task faulted.
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task.Run(Sub()
                                 Dim nestedTask1 = Task.Run(Sub()
                                                                Throw New CustomException("Detached child 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 CustomException Then
                    ' Recover from the exception. Here we just
                    ' print the message for demonstration purposes.
                    Console.WriteLine(ex.Message)
                End If
            Next
        End Try
    End Sub
End Module

Class CustomException : Inherits Exception
    Public Sub New(s As String)
        MyBase.New(s)
    End Sub
End Class
' The example displays the following output:
'       Detached child task faulted.

Mesmo se você usar uma continuação para observar uma exceção em uma tarefa filho, a exceção ainda deverá ser observada pela tarefa pai.

Exceções que indicam cancelamento cooperativo

Quando o código de usuário em uma tarefa responde a uma solicitação de cancelamento, o procedimento correto é lançar uma OperationCanceledException passagem no token de cancelamento no qual a solicitação foi comunicada. Antes de tentar propagar a exceção, a instância da tarefa compara o token na exceção com aquele que foi passado para ela quando foi criado. Se forem iguais, a tarefa propaga um TaskCanceledException embrulhado AggregateExceptionno , e isso pode ser visto quando as exceções internas são examinadas. No entanto, se o thread de chamada não estiver aguardando a tarefa, essa exceção específica não será propagada. Para obter mais informações, consulte Cancelamento de tarefas.

var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
var task = Task.Factory.StartNew(() =>
{
    CancellationToken ct = token;
    while (someCondition)
    {
        // Do some work...
        Thread.SpinWait(50_000);
        ct.ThrowIfCancellationRequested();
    }
},
token);

// No waiting required.
tokenSource.Dispose();
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)

Usando o método handle para filtrar exceções internas

Você pode usar o AggregateException.Handle método para filtrar exceções que você pode tratar como "manipuladas" sem usar nenhuma lógica adicional. No delegado de usuário fornecido ao AggregateException.Handle(Func<Exception,Boolean>) método, você pode examinar o tipo de exceção, sua Message propriedade ou qualquer outra informação sobre ele que permita determinar se ele é benigno. Todas as exceções para as quais o delegado retorna false são relançadas em uma nova AggregateException instância imediatamente após o retorno do AggregateException.Handle método.

O exemplo a seguir é funcionalmente equivalente ao primeiro exemplo neste tópico, que examina cada exceção na AggregateException.InnerExceptions coleção. Em vez disso, esse manipulador de exceções chama o AggregateException.Handle objeto de método para cada exceção e só relança exceções que não CustomException são instâncias.


public static partial class Program
{
    public static void HandleMethodThree()
    {
        var task = Task.Run(
            () => throw new CustomException("This exception is expected!"));

        try
        {
            task.Wait();
        }
        catch (AggregateException ae)
        {
            // Call the Handle method to handle the custom exception,
            // otherwise rethrow the exception.
            ae.Handle(ex =>
            {
                if (ex is CustomException)
                {
                    Console.WriteLine(ex.Message);
                }
                return ex is CustomException;
            });
        }
    }
}
// The example displays the following output:
//        This exception is expected!
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task.Run(Sub() Throw New CustomException("This exception is expected!"))

        Try
            task1.Wait()
        Catch ae As AggregateException
            ' Call the Handle method to handle the custom exception,
            ' otherwise rethrow the exception.
            ae.Handle(Function(e)
                          If TypeOf e Is CustomException Then
                              Console.WriteLine(e.Message)
                          End If
                          Return TypeOf e Is CustomException
                      End Function)
        End Try
    End Sub
End Module

Class CustomException : Inherits Exception
    Public Sub New(s As String)
        MyBase.New(s)
    End Sub
End Class
' The example displays the following output:
'       This exception is expected!

A seguir está um exemplo mais completo que usa o AggregateException.Handle método para fornecer tratamento especial para uma UnauthorizedAccessException exceção ao enumerar arquivos.

public static partial class Program
{
    public static void TaskException()
    {
        // This should throw an UnauthorizedAccessException.
        try
        {
            if (GetAllFiles(@"C:\") is { Length: > 0 } files)
            {
                foreach (var file in files)
                {
                    Console.WriteLine(file);
                }
            }
        }
        catch (AggregateException ae)
        {
            foreach (var ex in ae.InnerExceptions)
            {
                Console.WriteLine(
                    "{0}: {1}", ex.GetType().Name, ex.Message);
            }
        }
        Console.WriteLine();

        // This should throw an ArgumentException.
        try
        {
            foreach (var s in GetAllFiles(""))
            {
                Console.WriteLine(s);
            }
        }
        catch (AggregateException ae)
        {
            foreach (var ex in ae.InnerExceptions)
                Console.WriteLine(
                    "{0}: {1}", ex.GetType().Name, ex.Message);
        }
    }

    static string[] GetAllFiles(string path)
    {
        var task1 =
            Task.Run(() => Directory.GetFiles(
                path, "*.txt",
                SearchOption.AllDirectories));

        try
        {
            return task1.Result;
        }
        catch (AggregateException ae)
        {
            ae.Handle(x =>
            {
                // Handle an UnauthorizedAccessException
                if (x is UnauthorizedAccessException)
                {
                    Console.WriteLine(
                        "You do not have permission to access all folders in this path.");
                    Console.WriteLine(
                        "See your network administrator or try another path.");
                }
                return x is UnauthorizedAccessException;
            });
            return Array.Empty<string>();
        }
    }
}
// The example displays the following output:
//       You do not have permission to access all folders in this path.
//       See your network administrator or try another path.
//
//       ArgumentException: The path is not of a legal form.
Imports System.IO
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        ' This should throw an UnauthorizedAccessException.
        Try
            Dim files = GetAllFiles("C:\")
            If files IsNot Nothing Then
                For Each file In files
                    Console.WriteLine(file)
                Next
            End If
        Catch ae As AggregateException
            For Each ex In ae.InnerExceptions
                Console.WriteLine("{0}: {1}", ex.GetType().Name, ex.Message)
            Next
        End Try
        Console.WriteLine()

        ' This should throw an ArgumentException.
        Try
            For Each s In GetAllFiles("")
                Console.WriteLine(s)
            Next
        Catch ae As AggregateException
            For Each ex In ae.InnerExceptions
                Console.WriteLine("{0}: {1}", ex.GetType().Name, ex.Message)
            Next
        End Try
        Console.WriteLine()
    End Sub

    Function GetAllFiles(ByVal path As String) As String()
        Dim task1 = Task.Run(Function()
                                 Return Directory.GetFiles(path, "*.txt",
                                                           SearchOption.AllDirectories)
                             End Function)
        Try
            Return task1.Result
        Catch ae As AggregateException
            ae.Handle(Function(x)
                          ' Handle an UnauthorizedAccessException
                          If TypeOf x Is UnauthorizedAccessException Then
                              Console.WriteLine("You do not have permission to access all folders in this path.")
                              Console.WriteLine("See your network administrator or try another path.")
                          End If
                          Return TypeOf x Is UnauthorizedAccessException
                      End Function)
        End Try
        Return Array.Empty(Of String)()
    End Function
End Module
' The example displays the following output:
'       You do not have permission to access all folders in this path.
'       See your network administrator or try another path.
'
'       ArgumentException: The path is not of a legal form.

Observando exceções usando a propriedade Task.Exception

Se uma tarefa for concluída no estado, sua Exception propriedade poderá ser examinada TaskStatus.Faulted para descobrir qual exceção específica causou a falha. Uma boa maneira de observar a propriedade é usar uma continuação que é executada Exception somente se a tarefa antecedente falhar, como mostrado no exemplo a seguir.


public static partial class Program
{
    public static void ExceptionPropagationTwo()
    {
        _ = Task.Run(
            () => throw new CustomException("task1 faulted."))
            .ContinueWith(_ =>
            {
                if (_.Exception?.InnerException is { } inner)
                {
                    Console.WriteLine("{0}: {1}",
                        inner.GetType().Name,
                        inner.Message);
                }
            }, 
            TaskContinuationOptions.OnlyOnFaulted);
        
        Thread.Sleep(500);
    }
}
// The example displays output like the following:
//        CustomException: task1 faulted.
Imports System.Threading
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task.Factory.StartNew(Sub()
                                              Throw New CustomException("task1 faulted.")
                                          End Sub).
                    ContinueWith(Sub(t)
                                     Console.WriteLine("{0}: {1}",
                                                     t.Exception.InnerException.GetType().Name,
                                                     t.Exception.InnerException.Message)
                                 End Sub, TaskContinuationOptions.OnlyOnFaulted)

        Thread.Sleep(500)
    End Sub
End Module

Class CustomException : Inherits Exception
    Public Sub New(s As String)
        MyBase.New(s)
    End Sub
End Class
' The example displays output like the following:
'       CustomException: task1 faulted.

Em um aplicativo significativo, o delegado de continuação pode registrar informações detalhadas sobre a exceção e, possivelmente, gerar novas tarefas para recuperar da exceção. Se uma tarefa falhar, as seguintes expressões lançam a exceção:

  • await task
  • task.Wait()
  • task.Result
  • task.GetAwaiter().GetResult()

Use uma try-catch instrução para manipular e observar exceções lançadas. Em alternativa, observe a exceção acedendo ao Task.Exception imóvel.

Importante

O AggregateException não pode ser explicitamente capturado ao usar as seguintes expressões:

  • await task
  • task.GetAwaiter().GetResult()

Evento UnobservedTaskException

Em alguns cenários, como ao hospedar plug-ins não confiáveis, exceções benignas podem ser comuns e pode ser muito difícil observá-las manualmente. Nesses casos, você pode manipular o TaskScheduler.UnobservedTaskException evento. A System.Threading.Tasks.UnobservedTaskExceptionEventArgs instância que é passada para seu manipulador pode ser usada para impedir que a exceção não observada seja propagada de volta para o thread de junção.

Consulte também