Страницы

Поиск по вопросам

понедельник, 1 октября 2018 г.

Достоинства и недостатки использования async/await при работе с событиями

В этой теме прозвучала фраза, что работа с событиями по модели async/await имеет множество плюсов нежели традиционный событийных подход-подписался и забыл.
Теоретически, можно жить и без async/await. Но тогда у вас будет код разбросан по обработчикам событий, и состояние будет в виде глобальных переменных. А об обработке исключений я уже и не говорю — с ней будет совсем тяжко. Но да, как-то люди ж без async/await жили раньше, и в других языках до сих пор живут.
Собственно, меня это заинтересовало.
Хотелось бы получить информацию о всех достоинствах и возможно недостатках с примерами, традиционного подхода и подхода через async/await.


Ответ

Смотрите. Традиционый метод асинхронной работы — использование callback'ов. В каждой точке, где у вас await при этом вы должны завершить работу метода, подписавшись на продолжение окончание асинхронного кода. При этом вы должны где-то сохранить ваше состояние, то есть вы должны при этом таскать с собой локальные переменные вручную. Далее, логика циклов и условий тоже получается размазанной по нескольким кускам кода. Ну и делить на нужные части вам придётся вручную.
Вот пример простого async-кода: копирование потоков.
async Task CopyAsync(Stream source, Stream target, CancellationToken ct) { try { var buf = new byte[8192]; while (true) { var actuallyRead = await source.ReadAsync(buf, 0, buf.Length, ct); if (actuallyRead == 0) return; await target.WriteAsync(buf, 0, actuallyRead, ct); } } catch (OperationCanceledException) when (ct.IsCancellationRequested) { Debug.WriteLine("Cancelled"); } }
Ничего особенного нет.
Как нам написать ту же функциональность без синхронного кода, не занимая поток? У нас есть метод stream.BeginRead, который должен вернуть объект типа IAsyncResult. Попробуем смоделировать нашу функциональность таким же образом.
Для начала, нам нужно где-то хранить буфер, а также рабочие потоки. Для этого нам понадобится класс. Назовём его StreamCopyWorker, имея в виду, что логика работы будет тоже внутри него. Затем, мы хотим определить IAsyncResult. Объявим его отдельным классом, так как это всё же отдельная сущность.
В StreamCopyWorker должны быть методы BeginCopyAsync и EndCopyAsync. Имплементируем. Получается вот такой крокодил:
internal class StreamCopyWorker { internal readonly IAsyncResult Result; Stream source; Stream target; CancellationToken ct; ManualResetEventSlim ev = new ManualResetEventSlim(); AsyncCallback cb;
public StreamCopyWorker( Stream source, Stream target, object state, CancellationToken ct, AsyncCallback cb) { this.source = source; this.target = target; this.ct = ct; this.cb = cb; this.Result = new StreamCopyAsyncResult() { AsyncState = state, AsyncWaitHandle = ev.WaitHandle, self = this }; }
byte[] buf = new byte[8192];
internal void BeginAsync() { source.BeginRead(buf, 0, buf.Length, DoWrite, null); }
internal void EndAsync(IAsyncResult ar) { ct.ThrowIfCancellationRequested(); }
void DoWrite(IAsyncResult ar) { int bytesRead = source.EndRead(ar); if (bytesRead == 0 || ct.IsCancellationRequested) Finish(); else target.BeginWrite(buf, 0, bytesRead, DoRead, null); }
void DoRead(IAsyncResult ar) { target.EndWrite(ar); if (ct.IsCancellationRequested) Finish(); else BeginAsync(); }
void Finish() { ((StreamCopyAsyncResult)Result).IsCompleted = true; ev.Set(); cb(Result); }
internal class StreamCopyAsyncResult : IAsyncResult { public bool IsCompleted { get; internal set; } public WaitHandle AsyncWaitHandle { get; internal set; } public object AsyncState { get; internal set; } public bool CompletedSynchronously => false; internal StreamCopyWorker self { get; set; } } }
Ну и вспомогательные методы для вызова, чтобы спрятать создание класса:
IAsyncResult BeginCopyAsync(Stream source, Stream target, object state, CancellationToken ct, AsyncCallback cb) { var worker = new StreamCopyWorker(source, target, state, ct); worker.BeginAsync(); return worker.Result; }
void EndCopyAsync(IAsyncResult ar) { var result = (StreamCopyWorker.StreamCopyAsyncResult)ar; var worker = result.self; worker.EndAsync(ar); }
Вам всё ещё кажется, что без async/await легко?
С чистой событийной моделью получается спагетти ещё похлеще. Сейчас можно протягивать состояние через замыкания, и это немного упрощает код. Но не слишком. У меня та же задача на чистых событиях выглядит примерно так:
var buf = new byte[8192]; ResultCallback cb = (o, args) => { if (args.IsCancelled) Debug.WriteLine("Cancelled"); }; ReadHandler rhandler = null; rhandler = (o, args) => { source.ReadFinished -= rhandler;
if (ct.IsCancelationRequested) cb?.Invoke(null, new ResultArgs(isCancelled: true)); else { var readBytes = args.ReadBytes; if (readBytes == 0) cb?.Invoke(null, new ResultArgs(isCancelled: false)); else { WriteHandler whandler = null; whandler = (o, args) => { target.WriteFinished -= whandler; if (ct.IsCancelationRequested) cb?.Invoke(null, new ResultArgs(isCancelled: true)); else { source.ReadFinished += rhandler; source.ReadAsync(buf, 0, buf.Length); } }; target.WriteFinished += whandler; target.WriteAsync(buf, 0, readBytes); } } }; source.ReadFinished += rhandler; source.ReadAsync(buf, 0, buf.Length);
(плюс определение ResultCallback, ReadHandler, WriteHandler, ResultArgs и т. д.). Наверняка вы видели похожие, только более крупные «пирамиды смерти» в коде на Javascript.
Вы понимаете, что в этом коде творится? Я уже нет.

Комментариев нет:

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