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