Andrey on .NET | Превращаем асинхронные события в async/await

Превращаем асинхронные события в async/await

Пока еще в .NET существуют классы (особенно от сторонних разработчиков), которые используют старую модель для асинхронности. Речь идет реализации с помощью событий. Однако, преобразовать такой код к виду async/await очень легко.

Поможет в этом TaskCompletionSource<T>. Для примера возьмем некий класс Controller. Он содержит метод BeginRead() для начала операции асинхронного чтения данных. После её завершения пользователь может получить уведомление с помощью события Completed.

Стандартный для подобного подхода код выглядит примерно так:

var controller = new Controller();
controller.Completed += (s, e) => { /* обработка прочитанных данных */ };
controller.BeginRead();

Его можно переписать в стиле async/await c помощью своего метода-расширения:

public static async Task ReadAsync(this Controller controller)
{
    var tcs = new TaskCompletionSource<object>();
    EventHandler handler = (s, e) => tcs.TrySetResult(null);

    try {
        controller.Completed += handler;
        controller.BeginRead();
        await tcs.Task;
    }
    finally {
        controller.Completed -= handler;
    }
}

И теперь в коде приложения можно использовать более понятную запись:

await controller.ReadAsync();
/* обработка прочитанных данных */

Кстати, подобные методы-расширения можно создавать и для простого ожидания событий. Например:

public static async Task WaitForClick(this Button button)
{
    var tcs = new TaskCompletionSource<object>();
    EventHandler handler = (s, e) => tcs.TrySetResult(null);

    try {
        button.Click += handler;
        await tcs.Task;
    }
    finally {
        button.Click -= handler;
    }
}

Это позволит ожидать нажатия кнопки следующим вызовом:

await button.WaitForClick();

Комментарии (16) -

Алексей 02.07.2013 22:31:55

А как сделать возвращаемый тип Task<T> ?

@ Алексей: Подставить его, например c int:

public static async Task<int> ReadAsync(this Controller controller)
{
    var tcs = new TaskCompletionSource<int>();
    EventHandler handler = (s, e) => tcs.TrySetResult(someIntResultValue);

и дальше по тексту. Или вообще хочется generic?

Алексей 03.07.2013 0:27:00

@ Andrey:

ну да, хочется

var myTVal = await controller.ReadAsync();

ведь возвращаются не какие-то обстрактные данные, а object ковырять не хочется.

@ Алексей: Тут нет смысла в generic, т.к. каждый асинхронный "старый" метод возвращает свое. Есть два пути:
1) Предложенный в статье выше, тогда результат выдергиваем из самого класса.

2) Или как я предложил в комментарии: Пишем Task<конкретный тип> и в handler результат данного типа из объекта передаем в качестве параметра TrySetResult(). Тогда и можно var val = await controller.ReadAsync();

Алексей 03.07.2013 0:57:09

@ Andrey:

спасибо, это то, что нужно.

и у меня вопрос немного не по теме. существуют ли накладки при пробросе async/await методов? в новом Entity Framework появилось куча Async расширений, типо First() => FirstAsync() или Sum() => SumAsync(). но в типичной реализации приложения, напрямую к датаконтексту нет доступа, а через свою реализацию реппозитория, и есть ещё дата сервис, использующий реппозиторий. получается последовательный проброс async/await некоторых операций. вопрос, могут ли быть с этим проблемы? бьёт по производительности?

@ Алексей: IMHO все зависит от архитектуры приложения и объема запрашиваемых данных. Где-то синхронные запросы будут лучше, где-то асинхронные.

Опять же - как написаны репозиторий и сервисы. Task можно кэшировать и далеко не всегда обязательно создавать отдельный поток.

Ну и есть точка зрения что LINQ это DAL.

Алексей 03.07.2013 1:36:33

@ Andrey:

хорошая точка зрения, мне нравится Smile

но, я имею в виду следующее:

есть

async Task<Some> BaseService();


и два варианта:

1.
async Task<Some> Chain1() // call await BaseService
async Task<Some> Chain2() // call await Chain1
...
async Task<Some> Last() // call await ChainN

2.
Task<Some> Chain1() // call BaseService
Task<Some> Chain2() // call Chain1
...
async Task<Some> Last() // call ChainN

т.е. пробрасывать «чистый» Task или через await как правильнее? конечно без учета особенностей задач, теоретически

@ Алексей: Сама по себе пара async/await не создает Task. Поэтому оба стиля записи примерно равнозначные и без специфики метода я не буду выбирать лучший.

Может показаться, что более оптимально передать Task<T> по цепочке вверх, если результат, который дает Last(), уже в требуемом виде. Но тогда появляется вопрос - что делает вся эти цепочка? Какова её цель?

Алексей 03.07.2013 15:04:29

@ Andrey:

это могут быть обертки, как например это делается в ASP.NET MVC, где обертывается HttpContext в HttpContextBase (через HttpContextWrapper).

ещё методы интерфейсов нельзя пометить async, тесты показали, если самый верхний метод awaitable, то промежуточные методы могут пробрасывать Task<T> без async маркеров, это не повлияет на механизм async/await для вызываемого метода — и по тестам разницы по времени исполнения нет, надо будет копнуть глубже, на всякий случай Smile

@ Алексей: Я про длинные цепочки (адаптеры обычно не бывают такие "глубокие"). А так да - вернуть сам Task<T> ничего не мешает. Тут уже опять вопрос к контексту самих методов, т.к. по сути они будут выполняться синхронно и не смогут полагаться на результат вызовов того, что "под ними" по цепочке. (несколько запутанно пояснение вышло или понятно?)

Алексей 03.07.2013 16:31:57

@ Andrey:

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

Алексей 03.07.2013 16:37:00

@ Andrey:

или имелось в виду следующее:

public Task<Some> MethodInAMiddle()
{
   var result = TopMethod();
   // <-- вот здесь?
   return result;
}

// client
var result = await service.GetDataAsync();
/* в этом месте в result есть данные */

// service
public Task<DataType> GetDataAsync()
{
  Task<DataType> data = dal.GetDataAsync();
  ...
  return data;
}


Метод GetDataAsync выполнится синхронно, после чего уже клиент будет ждать когда отработает DAL. Кроме того, между dal.GetDataAsync() и return нет смысла обращаться за результатами в data (т.к. их еще нет).

Если сделать await dal.GetDataAsync(), то метод уже будет асинхронным и в нем можно работать с данными от dal.

Алексей 03.07.2013 16:52:40

@ Andrey:

все, теперь ясно, спасибо Smile надо делать async.

Не очень удобно объявлять сначала handler, а потом его подписывать. Можно же сразу лямбдой указать код. Или поведение кода станет другим (замыкание и пр.)?

Igor А как тогда вы будете отписывать лямбду?

Добавить комментарий