Страницы

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

среда, 27 ноября 2019 г.

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

#c_sharp #.net


В этой теме прозвучала фраза, что работа с событиями по модели async/await имеет
множество плюсов нежели традиционный событийных подход-подписался и забыл.


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


Собственно, меня это заинтересовало.

Хотелось бы получить информацию о всех достоинствах и возможно недостатках с примерами,
традиционного подхода и подхода через async/await.
    


Ответы

Ответ 1



Смотрите. Традиционый метод асинхронной работы — использование 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. Вы понимаете, что в этом коде творится? Я уже нет.

Ответ 2



Допустим, у нас есть задача для UI приложения: выполнить какую то логику, потом показать представление, дождаться, когда это представление будет закрыто и выполнить ещё что то. Как это решалось бы традиционным способом (я использую Window в качестве представления чисто для упрощения примера, в реальной задаче вместо окна может быть что угодно): int i = 0; void EventBased() { i = DoSmthg(); var wnd = new Wnd(); wnd.Closed+=Closed; wnd.Show(); } void Closed(object sender, EventArgs e) { (sender as Wnd).Closed -= Closed; DoSmthgElse(i); } Обратите внимание на 3 вещи: Логика казалось бы одного метода раскидана по нескольким методам. Это, конечно, можно исправить, назначив обработчик прямо на месте анонимным делегатом, но и там есть свои минусы. EventBased() метод неблокирующий. То есть тот, кто будет вызывать этот метод, не узнает об окончании работы всей логики. Это тоже решается добавлением фрейма в диспетчер или через wnd.ShowDialog() если wnd - окно (хотя это по сути также добавление фрейма), или новым событием, что тоже имеет свои минусы. Мы вынуждены хранить в поле i значение промежуточного результата. Это тоже можно было бы обойти анонимным делегатои, но это привело бы все равно к захвату переменной Как видите, желая решить казалось бы простую задачу, приходится изворачиваться, чтобы заставить код работать так, как требуется. Но давайте напишем такое окно, с которым работать будет проще: public class Wnd : Window { TaskCompletionSource s; public Wnd() { s = new TaskCompletionSource(); this.Closed+= (sender, args) => s.SetResult(this); } public Task ShowAsync() { this.Show(); return s.Task; } } Как видно, у окна теперь есть метод, который вернет таск. И этот таск завершится только когда окно будет закрыто. Теперь мы можем переписать вызывающий код следующим образом: async Task AsyncBased() { var i = DoSmthg(); var wnd = new Wnd(); await wnd.ShowAsync(); DoSmthgElse(i); } Больше нет необходимости шаманить с фреймами или делегатами или ещё с чем-либо. Вся нужная логика сосредоточена в одном методе (мы то знаем, что это не совсем так, но для читателя кода это верно), также вызывающий код может дождаться конца работы логики не прибегая к черной магии Промежуточное состояние выглядит как обычная локальная переменная Это просто один из простых примеров, что можно жить и без async/await, но использование async/await делает код немного лаконичней и понятней. Вы можете заменить окно чем угодно (получением данных из сети, записью в БД, любым асинхронным вызовом), окно взято чисто для примера работы с TaskCompletionSource.

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

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