30 марта 2013

Они смеются над твоими колбеками или async/await «для бедных»

.NET
У вас проект на .NET 4.0 и вам надоела «лапша» из колбеков? Вы бы хотели использовать async/await в своем проекте, но тимлид грозит небесной карой за смену платформы? Если установка патча на фреймворк и студию для вас являются допустимым решением, то вам сюда. Если нет, то существует другое решение.


(для гуру: внутри корутины на yield'ах)

Немного теории

Async-метод это сопрограмма, которая выходит на каждом await, и восстанавливается с этой точки по завершению ожидания(автор знает, что выполнение не всегда прерывается на await и что «ожидать» можно не только экземпляры Task). Итак, начиная со второй версии дотнета можно легко создавать сопрограммы с помощью ключевого слова yield. Этим инструментом мы и воспользуемся.

Концепт

Хочется писать как на C# 5.0, и при этом не ставить никаких языковых расширений. К сожалению так не получится. Но есть вот такой вариант:
private IEnumerable Login(...)
{
             // ...
             // get user id, if not specified
            if (string.IsNullOrEmpty(uid))
            {
                var getUserIdTask = сlient.GetUserId(...); yield return getUserIdTask; // await
                uid= getUserIdTask.Result.uid;
            }

            // login
            var loginTask = сlient.Login(...); yield return loginTask; // await
            var sessionId = loginTask.Result.SessionId;

            // getting user's profile
            var getUserInfoTask = сlient.GetUserInfo(...); yield return getUserInfoTask; // await
            var userInfo = getUserInfoTask.Result;            
            // ...
            
            yield return userInfo; // return
}

Всё что возвращается через yield return и не является наследником Task считается результатом исполнения async-метода.

Реализация

Код лежит тут.
Механизм работы простой:
  1. Создаем корневой Task и возвращаем вызывающему
  2. Вращаем итератор
  3. Если вернулся Task, то ждем его завершения через ContinueWith с переходом к шагу №2
  4. Если вернулась ошибка, то выставляем Exception для коневого Task
  5. Если вернулось значение, то завершаем коневой Task с данным результатом
  6. Если итератор кончился, то завершаем коневой Task со стандартным результатом

Во всех вариантах завершения на итераторе будет вызван Dispose, что приведет к освобождению ресурсов в блоках using и try/finally.

Начать новую асинхронную задачу можно вызвав метод FromIterator:
private IEnumerable Login(...) { ... }

Task loginTask = TaskUtils.FromIterator(this.Login(...));
// или с возвращаемым значением
Task<UserInfo> loginTask = TaskUtils.FromIterator<UserInfo>(this.Login(...));


Опционально можно указать:
  • state — состояние, которое попадет в Task.AsyncState
  • creationFlags — TaskCreationOptions для корневой задачи. Установка TaskCreationOptions.LongRunning говорит о том что вы очень не хотите блокировать текущий поток и вся работа должна быть выполнена в другом потоке
  • cancellationToken — токен прерывания процесса исполнения async-метода, передается вглубь везде, где это возможно


Заключение

Плюсы:

  • Компактненько и чисто
  • Можно не бояться за unmanaged ресурсы, using, try/catch работают
  • Не требует патчей и доп. библиотек
  • Будет работать в Mono


Минусы:

  • Async-метод возвращает IEnumerable, а не Task. Иногда приходится создавать дополнительный метод, который возвращает Task.
  • «Ожидать» можно только экземпляры Task
  • Ошибки «ожидаемых» задач нельзя обработать через try/catch(можно через ContinueWith). При ошибке в ожидаемой задаче, у корневой задачи выставляется Exception и поток исполнения больше не посещает async-метод.
  • Внутренний класс реализации(TaskBuilder) не порождает ссылок на себя, кроме тех случаев когда подписывается на ContinueWith в ожидаемых задачах, и есть вероятность сборки его GC

Большую часть минусов можно побороть тем или иным способом.

Ошибки по тексту можно направлять в ЛС.
Исходный код.
Теги:awaitasyncc#.net 4.0
Хабы: .NET
+12
38,6k 95
Комментарии 17
Лучшие публикации за сутки