Поделиться через


Обработка исключений (библиотека параллельных задач)

Необработанные исключения, созданные пользовательским кодом, который выполняется в задаче, распространяются обратно в присоединяемый поток, за исключением определенных сценариев, описанных далее в этом разделе. Исключения распространяются при использовании одного из статических методов или метода Task.Wait или Task<TResult>.Wait экземпляра и обрабатываются путем заключения вызова в оператор try-catch. Если задача является родительской задачей вложенных дочерних задач или ожидаются несколько задач, может быть сгенерировано несколько исключений. Чтобы распространить все исключения назад в вызывающий поток, инфраструктура задач заключает их в экземпляр AggregateException. Исключение AggregateException имеет свойство InnerExceptions, которое может быть перечислено для проверки всех созданных исходных исключений и обработки (или необработки) каждого исключения по отдельности. Даже если создано только одно исключение, оно, тем не менее, заключается в 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;
        }
    }

}

Чтобы избежать необработанного исключения, достаточно перехватить исключение AggregateException и не просматривать какие-либо внутренние исключения. Однако не рекомендуется делать это, поскольку это равнозначно перехвату базового типа исключения в непараллельных сценариях. Чтобы перехватить исключение без выполнения определенных действий для восстановления, можно оставить программу в неопределенном состоянии.

Если пользователь не ожидает задачи, распространяющей исключение, или получает доступ к свойству исключения, исключение расширяется согласно политике исключений .NET после сбора задачи сборщиком мусора.

Если исключения могут по восходящей маршрутизации возвращаться в присоединяемый поток, задача может продолжить обработку некоторых элементов после создания исключения.

ПримечаниеПримечание

Если включен режим "Только мой код", Visual Studio иногда прерывает выполнение программы на строке, в которой создается исключение, и отображает сообщение об ошибке "Исключение, которое не может быть обработано пользовательским кодом". Эта ошибка не является критической.Можно нажать F5, чтобы продолжить выполнение с этой ошибки. См. поведение системы при обработке этого исключения в примерах ниже.Чтобы предотвратить прерывание выполнения после первой ошибки в Visual Studio, необходимо снять флажок "Только мой код" в меню Сервис, Параметры, Отладка, Общие.

Вложенные дочерние задачи и вложенные AggregateExceptions

Если задача имеет вложенную дочернюю задачу, которая создает исключение, это исключение заключается в исключение AggregateException перед распространением в родительскую задачу, которая заключает его в собственное исключение AggregateException перед распространением назад в вызывающий поток. В таких случаях свойство AggregateException().InnerExceptions исключения AggregateException, перехватываемого в методе Task.Wait, Task<TResult>.Wait, WaitAny или WaitAll, содержит один или несколько экземпляров AggregateException, а не исходные исключения, вызвавшие сбой. Чтобы избежать выполнения итерации по вложенным исключениям AggregateExceptions, можно использовать метод Flatten() для удаления всех вложенных исключений AggregateExceptions, чтобы свойство AggregateException() InnerExceptions содержало исходные исключения. В следующем примере вложенные экземпляры AggregateException выравниваются и обрабатывается только в одном цикле.

' 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);

}

Исключения из отсоединенных дочерних задач

По умолчанию дочерние задачи создаются отсоединенными. Исключения, созданные из отсоединенных задач, должны обрабатываться или повторно создаваться в непосредственной родительской задаче. Они не распространяются назад в вызывающий поток так же, как вложенные дочерние задачи, распространяемые обратно. Верхняя родительская задача может вручную повторно создать исключение из отсоединенной дочерней задачи, чтобы заключить его в AggregateException и распространить назад в присоединяемый поток.

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);
        }
    }
}

Даже если для просмотра исключения в дочерней задаче используется продолжение, исключение по-прежнему должно рассматриваться в родительской задаче.

Исключения, указывающие согласованную отмену

Когда пользовательский код в задаче отвечает на запрос отмены, правильной процедурой будет создание исключения OperationCanceledException и передачи его в токен отмены, на котором был передан запрос. Перед попыткой распространить исключение экземпляр задачи сравнивает токен в исключении с токеном, переданным в него при его создании. Если они совпадают, задача распространяет исключение TaskCanceledException, заключенное в AggregateException, и его можно просмотреть при проверке внутренних исключений. Однако если присоединяемый поток не ожидает задачу, это конкретное исключение не будет распространяться. Дополнительные сведения см. в разделе Отмена задач.

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.

Использование метода обработки для фильтрации внутренних исключений

Метод Handle() можно использовать для фильтрации исключений, которые можно рассматривать как обработанные без использования какой-либо дальнейшей логики. В пользовательском делегате, который предоставляется Handle(), можно проверить тип исключения или его свойство Message(), или другие сведения о нем, которые позволят определить, является ли он благоприятным. Любые исключения, для которых делегат возвращает значение false, повторно создаются в новом экземпляре AggregateException сразу после возврата Handle().

В следующем фрагменте используется цикл foreach внутренних исключений.

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;
    }
}

В следующем фрагменте показано функционально эквивалентное использование метода Handle().

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

Наблюдение за исключениями с помощью свойства Task.Exception

Если задача завершается в состоянии Faulted, можно проверить ее свойство Exception, чтобы узнать, какое именно исключение вызвало сбой. Для того, чтобы просмотреть свойство Exception, рекомендуется использовать продолжение, выполняемое только в случае сбоя предшествующей задачи, как показано в следующем примере.

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);

В реальном приложении делегат продолжения может записать подробные сведения об исключении и, возможно, создать новые задачи для восстановления из исключения.

Событие UnobservedTaskException

В некоторых сценариях (например, при размещении недоверенных подключаемых модулей) часто происходят некритические исключения и наблюдение за всеми этими исключениями вручную может оказаться проблематичным. В этих ситуациях можно обработать событие TaskScheduler.UnobservedTaskException. Экземпляр System.Threading.Tasks.UnobservedTaskExceptionEventArgs, передаваемый обработчику, можно использовать для недопущения распространения непредвиденного исключения обратно в присоединяемый поток.

См. также

Основные понятия

Библиотека параллельных задач