Исключения в задачах
В отличие от потоков задачи без труда распространяют исключения. Таким образом, если код внутри задачи генерирует необработанное исключение (другими словами, если задача отказывает), то это исключение автоматически повторно сгенерируется при вызове метода Wait или доступе к свойству Result класса Task<TResult>. В посте детально раскрывается механизм обработки данных исключений.
Исключения
Представим код, в котором запущенная задача генерирует исключение NullReferenceException
.
1
2
3
4
5
6
7
8
9
10
11
12
Task task = Task.Run (() => { throw null; });
try
{
task.Wait();
}
catch (AggregateException aex)
{
if (aex.InnerException is NullReferenceException)
Console.WriteLine ("Null!");
else
throw;
}
Проверить, отказала ли задача, можно без повторной генерации исключения посредством свойств IsFaulted
и IsCanceled
класса Task
. Если оба свойства возвращают false
, то ошибки не возникали; если IsCanceled
равно true
, то для задачи было сгенерировано исключение OperationCanceledException
; если IsFaulted
равно true
, то было сгенерировано исключение другого типа и на ошибку укажет свойство Exception
.
Так как множество запущенных задач задействуют множество потоков, то вполне возможна одновременная генерация двух и более исключений. Чтобы обеспечить получение сведений обо всех исключениях, они помещаются в контейнер AggregateException
, свойство InnerExceptions
которого содержит каждое из перехваченных исключений:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Task t1 = new Task(() =>
{
throw new OutOfMemoryException();
});
Task t2 = new Task(() =>
{
throw new DivideByZeroException();
});
t1.Start(); t2.Start();
try
{
Task.WaitAll(t1, t2);
}
catch (AggregateException aex)
{
foreach (Exception ex in aex.InnerExceptions)
Console.WriteLine(ex.Message);
}
Flatten и Handle
Класс AggregateException
предоставляет пару методов для упрощения обработки исключений: Flatten
и Handle
.
Flatten
Если у задачи есть вложенные дочерние задачи, то объект exc
представляет собой опять тип AggregateException
и для обработки исключений дочерних задач необходимо во вложенном цикле обрабатывать элементы exc.InnerExceptions
. Метод Flatten
объекта AggregateException
возвращает все исключения, возникнувшие в задачах и вложенных задачах в одном списке, делая более удобной обработку всех исключений.
1
2
3
4
5
6
7
8
9
10
11
12
13
Task[] tasks = new Task[N];
// Объявляем и запускаем задачи
..
// Обработка исключений
try
{
Task.WaitAll(tasks);
}
catch (AggregateException ae)
{
foreach (Exception e in ae.Flatten().InnerExceptions)
Console.WriteLine("Message:{ 0}", e.Message);
}
Handle
Иногда полезно перехватывать исключения только специфических типов, а исключения других типов генерировать повторно. Метод Handle
класса AggregateException
предлагает удобное сокращение. Он принимает предикат исключений, который будет запускаться на каждом внутреннем исключении:
1
public void Handle (Func<Exception, bool> predicate)
Если предикат возвращает true
, то считается, что исключение “обработано”. После того, как делегат запустится на всех исключениях, произойдет следующее:
- если все исключения были “обработаны” (делегат возвратил
true
), то исключение не генерируется повторно; - если были исключения, для которых делегат возвратил
false
(“необработанные”), то строится новый объектAggregateException
, содержащий такие исключения, и затем он генерируется повторно. Например, приведенный далее код в конечном итоге повторно генерирует другой объектAggregateException
, который содержит одиночное исключениеNullReferenceException
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var parent = Task.Factory.StartNew (() =>
{
// We’ll throw 3 exceptions at once using 3 child tasks:
int[] numbers = { 0 };
var childFactory = new TaskFactory
(TaskCreationOptions.AttachedToParent, TaskContinuationOptions.None);
childFactory.StartNew (() => 5 / numbers[0]); // Division by zero
childFactory.StartNew (() => numbers [1]); // Index out of range
childFactory.StartNew (() => { throw null; }); // Null reference
});
try { parent.Wait(); }
catch (AggregateException aex)
{
aex.Flatten().Handle (ex => // Note that we still need to call Flatten
{
if (ex is DivideByZeroException)
{
Console.WriteLine ("Divide by zero");
return true; // This exception is "handled"
}
if (ex is IndexOutOfRangeException)
{
Console.WriteLine ("Index out of range");
return true; // This exception is "handled"
}
return false; // All other exceptions will get rethrown
});
}
Исключения и автономные задачи 1
В автономных задачах, работающих по принципу “установить и забыть” (для которых не требуется взаимодействие через метод Wait
или свойство Result
либо продолжение, делающее то же самое), общепринятой практикой является явное написание кода обработки исключений во избежание молчаливого отказа (в точности, как с фоновым потоком).
Подписаться на необнаруженные исключения на глобальном уровне можно через статическое событие TaskScheduler.UnobservedTaskException
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static void Main()
{
TaskScheduler.UnobservedTaskException +=
(object sender, UnobservedTaskExceptionEventArgs eventArgs) =>
{
((AggregateException)eventArgs.Exception).Handle(ex =>
{
Console.WriteLine("Exception: {0}", ex.Message);
return true;
});
};
Task.Factory.StartNew(() =>
{
throw new ArgumentNullException();
});
Task.Factory.StartNew(() =>
{
throw new ArgumentOutOfRangeException();
});
Thread.Sleep(100);
GC.Collect();
Console.WriteLine("Done");
}
// ВЫВОД:
// Exception: Значение не может быть неопределенным.
// Exception: Заданный аргумент находится вне диапазона допустимых значений.
// Done
Событие TaskScheduler.UnobservedTaskException
происходит, когда сборщик мусора обнаруживает, что объект задачи (Task
), содержащий необработанное исключение, становится недостижимым (то есть к нему больше нет активных ссылок и он подлежит утилизации). Это означает, что исключение, возникшее внутри задачи, не было “наблюдаемым” – другими словами, не было поймано или обработано через механизмы ожидания завершения задачи (Wait
, Result
) или через обработку состояний (ContinueWith
при условии обработки исключения, доступ к свойству Exception
или проверка IsFaulted
).
Важные моменты, которые стоит помнить о TaskScheduler.UnobservedTaskException
:
- Обработчик этого события дает последний шанс обработать исключение, прежде чем оно будет “проигнорировано”. Это может быть полезно для логирования ошибок или для предотвращения аварийного завершения приложения из-за необработанных исключений.
Если исключение не помечено как “наблюдаемое” через обработчик события
UnobservedTaskException
(например, если обработчик не вызывает методSetObserved
объектаUnobservedTaskExceptionEventArgs
), сборщик мусора считает, что исключение не обработано. В зависимости от версии .NET и настроек приложения это может привести к аварийному завершению процесса.В приведенном выше коде, использование
.Handle(...)
внутри обработчикаTaskScheduler.UnobservedTaskException
делает явный вызовSetObserved
необязательным.- Событие вызывается асинхронно и не обязательно сразу после того, как задача становится недостижимой. Точное время вызова зависит от того, когда сборщик мусора решит выполнить чистку.
Игнорирование исключений нормально в ситуации, когда исключение только указывает на неудачу при получении результата, который больше не интересует. Например, если пользователь отменяет запрос на загрузку веб-страницы, то мы не должны переживать, если выяснится, что веб-страница не существует.
Игнорирование исключений проблематично, когда исключение указывает на ошибку в программе, по двум причинам:
- ошибка может оставить программу в недопустимом состоянии;
- в результате ошибки позже могут возникнуть другие исключения, и отказ от регистрации первоначальной ошибки может затруднить диагностику.
Есть пара интересных нюансов относительно того, какое исключение считать необнаруженным.
- Задачи, ожидающие с указанием тайм-аута, будут генерировать необнаруженное исключение, если ошибки возникают после истечения интервала тайм-аута.
- Действие по проверке свойства
Exception
задачи после ее отказа помечает исключение как обнаруженное.
Использованный источник
Албахари Д., Албахари Б. C# 7.0. Справочник. Полное описание языка.: Пер. с англ. – СпБ.: ООО «Альфа-книга», 2018. – С. 577-578. (См. главу 14. Параллелизм и асинхронность, п. Исключения.) ↩︎