MCP

суббота, 29 июня 2013 г.

Эксепшены в Task'ах

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

Конкретно речь пойдёт про Таски. Не буду про них расписывать, ибо не это тема сегодняшнего поста. Вкратце, это такие треды на стероидах с вкусными и удобными фичам.

Одна из фич, это проброс эксепшенов наружу. Примерно так:

var t = new Task(() => { throw new Exception(); });
t.Start();
// do something
try
{
 t.Wait();
}
catch (AggregateException)
{
 Console.WriteLine("Exception occurred");
 throw;
}

Т.е. мы что-то выполняем в отдельном потоке, в это время занимаемся своими вещами, а потом убеждаемся, что таск закончился и ловим исключение, которое за нас обернули в AggregateException (настоящее скрыто внутри его).
Т.е. получается вполне прикольная фича, можно честно ловить исключения из других потоков в то время, когда мы знаем, как их обработать. И этим можно активно пользоваться (например, скрывая всю параллельную работу внутри метода, но пробрасывая исключения наружу).

Но есть маленький нюанс. Эксепшен ведь пробрасывается, когда от таска что-то ждут (результат, или просто завершение). В стандартной терминологии это называется observable. Т.е. таск должен быть наблюдаемым. А что если мы запустили и забыли (или забили)?

new Task(() => { throw new Exception(); }).Start();

Получается, что эксепшен куда-то потеряется? Как бы не так! Доблестные спецы из Microsoft подумали, и придумали бросать исключение в деструкторе. Оцените изящество этого решения: нарушает все вменяемые стандарты кодирования, приводит к падению приложения, и выдаёт ошибки в то время и в том месте, где их отловить невозможно вообще. Супер!

Наверное, они решили, что раз исключение в обычном треде вызывает падение приложения, то и тут надо. Только забыли про это маленькое отличие с исключениями: если мы хотим сделать безопасным обычный тред, нам достаточно написать try/catch и все станут счастливы (всё равно никто за нас ошибки не обработает). А случае с тасками, кидать исключение изнутри наружу можно и местами иногда полезно. Но получается, что за этими тасками приходится следить как за маленькими детьми, хотя нам вполне может быть глубоко наплевать в данном месте на результат конкретной операции.

Решений данной проблемы в принципе можно придумать несколько (различные врапперы, следить за использованием тасков), одно из достаточно красивых такое:

public static Task IgnoreErrors(this Task t)
{
 t.ContinueWith(x => { var e = x.Exception; }, TaskContinuationOptions.OnlyOnFaulted);
 return t;
}

private static void DoTask()
{
 new Task(() => { throw new Exception(); }).IgnoreErrors().Start();
}

Т.е. мы создаём extension-метод, который прикрепляет к нашей таске продолжение (continuation), в котором мы берём значение свойства Exception (это обязательно надо сделать! именно так таска становится опять наблюдаемой (observable)), и на этом успокаиваем систему.

Задача решена, и надеюсь, что у вас она вызовет мата меньше чем у меня в своё время, когда я попался на такую проблему. А попался я на неё ещё по одной причине.

Бонус

Я вначале не зря писал, что в Microsoft вначале сделали, а потом начали думать. А подумав, они поняли, что поведение уж больно идиотское и убивать всё приложение никому не нужно. И в 4.5 изменили логику, так что для 4.5 уже мой пост неактуален.

Но без нюансов у Microsoft не обходится, ибо 4.5 по факту подменяет собой 4.0. Сюрприз! Т.е. установив 4.5, все приложения, написанные под 4.0 начинают по факту работать на 4.5, и слегка по-другому. Например, в данном случае, ошибка уже не появится (попадаются и некоторые другие различия в поведении, но это уж слишком выраженное). Вернуть старое поведение можно конфигом, но кто же заморачивается чтением всяких безумных и бесполезных настроек?

Т.е. вы как разработчик поставили все апдейты, написали приложение, протестировали в хвост и в гриву, а падает оно только у клиента. Ибо у клиента стоит Windows Server 2003, и ваше приложение должно с ним работать, и даже работает, только падает. Иногда. С непонятным стэктрейсом. И вы пытаетесь найти хвосты и поправить всё это безобразие.

Иногда просто хочется убиться головой об стену от таких гениальных поворотов сюжета.