Async\Await методы и PostSharp

Сложность 300-400

Описание проблемы

Уже много чего написал насчет PostSharp, конкретных решений, кастом компонентов, общие принципы работы. Однако с выходом .Net 4.5 появилась новая фича со специальными словами async\await которые позволяют более просто и компактно описывать асинхронное поведение методов. Новый функционал хорош, но добавляет головной боли при использовании PostSharp, с методами помеченными async, использование классических аспектов не пройдет. Методы с маркером async разворачиваются в машину состояний, что не очень хорошо с точки зрения применения аспектов. Т.е. возьмем стандартную реализацию аспекта трассировщика с помощью  PostSharp:

И применим его к тестовому примеру с вызовом синхронных и асинхронных методов с исключениями и без:

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

По картинке можно заметить, что порядок логирования неверен для асинхронных методов. Логирование выхода происходит раньше, чем реально заканчивается работа метода. Это можно увидеть для методов SecondAsync(), AsyncVoidThird(). Кроме этого зная код и глядя на вывод можно заметить, что произошло только одно исключение. Это большая проблема методов void async о чем будет специально упомянуто ниже.

Рассмотрим, как инструментализируется аспектом обычный метод:

Здесь все хорошо, так как и должно быть. А теперь обратим взор на метод с маркером async:

В данном случае реализация выглядит посложнее с машиной состояний. К тому же видно, что метод OnSucess() вызывается в момент запуска задачи, а не завершения. Т.е. мы отслеживаем по факту только успешность\не успешность запуска задачи, а не самого хода выполнения задачи. Согласитесь, что ход выполнения задачи интересует в разы больше.

Возможное решение

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

Нам понадобится новый базовый класс для работы с методами помеченными маркером async:

Для удобства работы используется проверка на этапе компиляции. Проверяем, что аспект будет применен к методу, который возвращает тип Task. Ничего сложного. Самое интересное в методе OnExti(), где мы перехватываем и назначаем продолжения к задаче в зависимости от результата ее выполнения.

Далее оформлены привычные методы для окончания задачи, окончания задачи с ошибкой и если метод закончен успешно.

Теперь можно попробовать написать еще один аспект на основе только что созданного:

Вообще все то же самое, кроме базового класса. Однако результат кардинально различается!

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

 

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

Для того, чтобы сообщения выводились по порядку (в широком смысле, чтобы все действия аспекта совершились до того, как метод «официально» завершен для остальной программы), можно дополнительно написать аспект, либо ввести параметр для указания синхронности выполнения «хвостовых» методов из аспекта. Для это стоит модифицировать метод OnExit() в базовом классе, добавить опцию TaskContinuationOptions.ExecuteSynchronously.

Применение тогда будет выглядеть так:

А итоговый вывод для методов с async следующим образом:

Отлично, логирование для метода SecondAsync() выглядит точно так, как мы и ожидали увидеть.

Но не все так радужно, все равно остаются сложности для методов с сигнатурой async void, с ними возникает больше всего проблем. С ними ничего не будет работать, так  как просто нет возвращаемого типа. Возможно с использованием Roslyn можно будет победить данную проблему, но пока до этого руки не дошли.

Остановимся на них немного подробнее, как и обещал.

Async void

В общем случае, если вы видите async void в вашем коде, то у меня для вас плохие новости:

  • Вы не можете ожидать завершения такого метода (не очень страшно иногда)
  • Любое не перехваченное исключение будет убивать ваше приложение (полный П)

Представим, что у нас есть таймер, который запускает какое-то действие и вдруг оно вызывает исключение. В каком-то методе у нас есть код для запуска таймера:

И, конечно, есть реализация метода:

То, что таймер сломается может быть не так критично, но вот падение приложения это уже серьезная проблема.

Какие тут могут быть пути для решения проблемы? Можно поменять обработчик события, чтобы возвращался объект типа Task, но это не будет компилироваться так как в ElapsedEventHandler определено, что делегат должен возвращать void.

Тогда на ум приходят 2 возможности:

  1. Можно выполнить весь блок синхронно
  2. Обернуть вызов в продолжение с ничего не деланьем. (как-то так)

Синхронный вызов не является реальным способом.

При таком вызове поток не вернет управление и будет дожидаться окончания операции. Теряется вообще весь смысл в async\await.

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

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

Итого

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

 

Hard’n’Heavy!

 

 

Violet Tape

Оставить комментарий