День двести семнадцатый. #ЗаметкиНаПолях

Многопоточность.

9. Async/await. Продолжение

Обработка исключений

Когда вы ожидаете завершения асинхронной операции, то возможно, она давным-давно завершилась неудачей в совершенно другом потоке. В этом случае обычный синхронный способ распространения исключений в стеке не подходит. Вместо этого инфраструктура async/await предпринимает некоторые шаги, чтобы опыт обработки асинхронных сбоев был максимально похож на синхронные сбои.

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

Task и Task<TResult> указывают на сбои несколькими способами:

- Свойство Status становится Faulted, если асинхронная операция не выполнена (а IsFaulted возвращает значение true).

- Свойство Exception возвращает AggregateException, которое содержит все (возможно несколько) исключений, которые привели к сбою задачи, либо null если задача завершилась успешно.

- Метод Wait() выбрасывает исключение AggregateException, если задача заканчивается неудачей.

- Свойство Result для Task<TResult> (которое также приводит к ожиданию завершения задачи) аналогично выбрасывает AggregateException.

Если задача отменяется через CancellationToken, метод Wait() и свойство Result генерируют исключение AggregateException, содержащее исключение OperationCanceledException, но при этом свойство Status устанавливается в Canceled (см. пост про отмену задания https://t.me/NetDeveloperDiary/219).

При вызове await, если задача завершается неудачей или отменяется, выбрасывается исключение, но не AggregateException. Вместо этого для удобства выбрасывается первое исключение из коллекции AggregateException. В большинстве случаев это то, что вам нужно. Однако это может привести к потере информации. Если в задаче возникает несколько исключений, GetResult выдаёт только первое из них. Возможно, вы захотите переписать код, чтобы при сбое вызывающая сторона могла перехватить AggregateException и изучить все причины сбоя. Некоторые методы фреймворка делают это. Например, Task.WhenAll(), который асинхронно ожидает завершения всех задач, указанных в аргументе метода. Например:

var tasks = new Task<string>[]{ … };



Task<string[]> results = Task.WhenAll(tasks);

foreach (var result in results.Result) {…}

Если какие-либо из задач дают сбой, результатом вызова results.Result является AggregateException, который будет содержать исключения из всех задач, завершившихся неудачей. Но если вы вызовете await для Task.WhenAll(), вы увидите только первое исключение:

string[] results = await Task.WhenAll(tasks);



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

Предположим, вы хотите выполнить некоторую работу в асинхронном методе после проверки параметра на ненулевое значение:

Task<int> task = DoSomeWork(null);



int result = await task;

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

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



Продолжение следует…



Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 5.