Pull to refresh

Обработка исключений в асинхронном коде при переходе на .NET 4.5

Reading time 3 min
Views 19K
В посте я попытаюсь раскрыть подводные камни, которые возникают при обработке исключений в асинхронном коде в .NET 4 в контексте .NET 4.5

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

Рассмотрим, как будет себя вести код из примера в зависимости от того .NET каких версий установлен на сервере:

class Program
{
    static void Main(string[] args)
    {
        Task.Factory.StartNew(() => { throw new Exception(); });

        Thread.Sleep(1000);

        GC.Collect();
    }
}


Если на сервере установлен .NET 4 и не установлен .NET 4.5, процесс завершится вследствие необработанного исключения. Генерация исключений из задач, завершения которых никто не ожидает, происходит при сборке мусора. Если бы пример выглядел так:

class Program
{
    static void Main(string[] args)
    {
        Task.Factory.StartNew(() => { throw new Exception(); });

        Thread.Sleep(1000);
    }
}


То проблему было бы тяжело заметить.

Для обработки таких исключений у типа TaskScheduler есть событие TaskUnobservedException. Событие позволяет перехватить исключение и предотвратить завершение процесса.

Если на сервере установлен .NET 4.5, поведение меняется, процесс не завершается вследствие необработанного исключения.
Если в .NET 4.5 осталось бы стандартное поведение из .NET 4, то в примере ниже при сборке мусора после вызова метода SomeMethod процесс бы завершился, т.к. исключение из Async2() осталось бы необработанным:

public static async Task SomeMethod()
{
    try
    {
        var t1 = Async1();
        var t2 = Async2();

        await t1;
        await t2;
    }
    catch 
    {
        
    }
}

public static Task Async1()
{
    return Task.Factory.StartNew(() => { throw new Exception(); });
}

public static Task Async2()
{
    return Task.Factory.StartNew(() => { throw new Exception(); });
}


Чтобы после установки .NET 4.5 вернуть стандартное поведение из .NET 4, необходимо добавить в конфигурационный файл приложения ключ ThrowUnobservedTaskExceptions.

На практике подобное изменение поведения при переходе от одной версии фреймворка к другой опасно тем, что на лайве может быть не установлен .NET 4.5, а разработчик работает в системе с .NET 4.5. В этом случае разработчик может пропустить подобную ошибку. Поэтому при разработке настоятельно рекомендуется тестировать приложение с включенным ключом ThrowUnobservedTaskExceptions.

В .NET 4.5 есть еще одно нововведение, которое может доставить немало проблем — async void методы, которые иначе обрабатываются компилятором, нежели async Task методы. Для обработки async void методов используется AsyncVoidMethodBuilder, а для async Task методов AsyncTaskMethodBuilder. При возникновении ошибок, в случае если нет контекста синхронизации, исключение будет выброшено в пул потоков, что ведет к завершению процесса. Такое исключение можно перехватить, но предотвратить завершение процесса не получится. async void методы должны использоваться только для обработки событий от UI элементов.

Пример неочевидного использования async void метода, которое приводит к завершению процесса:

new List<int>().ForEach(async i => { throw new Exception(); });

Если у вас нет необходимости в async void методах, можно в CI добавить правило, которое при появлении в IL использования AsyncVoidMethodBuilder, считало бы билд неуспешным.

Источники:
  1. http://www.jaylee.org/post/2012/07/08/c-sharp-async-tips-and-tricks-part-2-async-void.aspx
  2. http://blogs.msdn.com/b/cellfish/archive/2012/10/18/the-tale-of-an-unobservedtaskexception.aspx
  3. http://blogs.msdn.com/b/pfxteam/archive/2011/09/28/10217876.aspx
Tags:
Hubs:
+28
Comments 15
Comments Comments 15

Articles