Думаю что было бы наивно не обсудить то, про что нам рассказали на последнем PDC — а рассказали, к сожалению немного. В этом посте речь в основном пойдет о async/await
, хотя пользуясь случаем, постараюсь затронуть и другие темы.
Что было
До добавления поддержки continuations в Async CTP, у нас было 2 варианта реализации “продолжений” в .Net-приложениях (имеется ввиду, помимо callback’ов).
Вариант 1й – это использование F# async workflows. Идея тут примитивна – workflow в F# этот некий scope в котором переопределено поведение операторов. F# хитро определял пары BeginInvoke()/EndInvoke()
…
type WebRequest with member x.GetResponseAsync() = Async.BuildPrimitive(x.BeginGetResponse, x.EndGetResponse)
…а потом позволял использовать операторы let!
и return!
чтобы “дождаться” выполнения callback’а.
let private DownloadPage(url:string) = async { try let r = WebRequest.Create(url) let! resp = r.GetResponseAsync() // let! позволяет дождаться результата use stream = resp.GetResponseStream() use reader = new StreamReader(stream) let html = reader.ReadToEnd() use fs = new FileStream(@"c:\temp\file.htm", FileMode.Create, FileAccess.Write, FileShare.None, 1024, true); let bytes = Encoding.UTF8.GetBytes(html); do! fs.AsyncWrite(bytes, 0, bytes.Length) // ждем пока все запишется with | :? WebException -> () } Async.RunSynchronously(DownloadPage("http://devtalk.net"))
Вариант 2й придумал Джеффри Рихтер, создав AsyncEnumerator
который использовал для прерываний и продолжений ключевое слово yield
. Идея хорошая, но подобное поведение не работаем во многих случаях – например в try-catch
.
public IEnumerator<int> DownloadPage(string url, AsyncEnumerator ae) { var wr = WebRequest.Create(url); wr.BeginGetResponse(ae.End(), null); yield return 1; var resp = wr.EndGetResponse(ae.DequeueAsyncResult()); var stream = resp.GetResponseStream(); var reader = new StreamReader(stream); string html = reader.ReadToEnd(); using (var fs = new FileStream(@"c:\temp\file.htm", FileMode.Create, FileAccess.Write, FileShare.None, 1024, true)) { var bytes = Encoding.UTF8.GetBytes(html); fs.BeginWrite(bytes, 0, bytes.Length, ae.End(), null); yield return 1; fs.EndWrite(ae.DequeueAsyncResult()); } }
Как раз Джеффри и консультировал Microsoft по вопросам Async CTP – вы помните что еще давным-давно MS обжало лицензию PowerThreading, запретив использование этой библиотеки на не-Windows системах? Не знаю в чем там была соль, но сотрудничество продолжилось и теперь мы можем лицезреть во что оно вылилось.
Что стало
В C# и VB добавлена точно такая же поддержка, только механизмы другие. На самом деле, все просто до неприличия.
Во-первых, у нас появилась возможность “дожидаться”. Для этого введено новое ключевое слово await
, которое как аргумент берет Task
. Это такой класс из TPL который может инкапсулировать кусок работы, и при этом заменеджить отмену (через CancelationTokenSource
) и исключения (через AggregateException
).
Во-вторых, у нас появилась возможность определять асинхроные методы не через пару Begin-End, а через ключевое слово async
. Это избавляет нас от мучений с IAsyncResult
. Теперь, все что нужно – это при определении метода написать ключевое слово async
и изменить тип возвращаемого значения на Task
.
Ключевые слова async
и await
завязаны друг на друга – если метод использует await
, то он должен быть помечен как async
.
Ну вот, давайте теперь посмотрим на “живой” пример все того же скачивания данных с сайта и записи их в файл – только теперь с использованием async/await:
public static async Task DownloadInformation(int id) { Contract.Requires(id >= 0); string filename = @"c:\temp\somesite.com\" + id + ".htm"; if (File.Exists(filename)) return; var ct = new CancellationToken(); var r = new Reporter(id); string data = await new WebClient().DownloadStringTaskAsync( new Uri(@"http://somesite.com/" + id), ct, r); using (var fs = new FileStream(filename, FileMode.CreateNew, FileAccess.Write, FileShare.Write)) { var bytes = Encoding.Default.GetBytes(data); await fs.WriteAsync(bytes, 0, bytes.Length); fs.Close(); } }
Первый парадокс восприятия в том, что метод ничего не возвращает, но не помечен как void
. Это еще один трюк – не знаю насколько уместный. Суть в том, что метод который использует await
должен быть помечен как async
и возвращать Task
(или Task<T>
). Соответственно если бы мой метод выше возвращал int
, то в сигнатуре было бы написано Task<int>
.
Итак, помимо привычных методов, Async CTP добавил классу WebClient
метод расширения DownloadStringTaskAsync
, что как бы намекает. Также он добавил метод WriteAsync
классу FileStream
, хотя было бы намного лучше (а нам это обещали, нет?) если бы просто был метод File.WriteAllTextAsync()
. Так или иначе, обоих методов можно “дождаться” с помощью await
.
Кстати о птичках – мне понравилась возможность подсунуть в DownloadStringTaskAsync()
нечто реализующее IProgress<>
– мониторить прогресс скачивания файла нужно многим, а тут как раз и сделали поддержку именно этого. Собственно класс Reporter
из примера выше выглядит вот так:
private class Reporter : IProgress<DownloadProgressChangedEventArgs> { private int id; public Reporter(int id) { this.id = id; } public void Report(DownloadProgressChangedEventArgs value) { Console.WriteLine("{0}:{1}", id, string.Empty.PadRight(value.ProgressPercentage / 10, '*')); } }
Теперь о том как все это вызвать. Вот мой подход:
static async void Main() { var ct = new CancellationTokenSource(); Console.CancelKeyPress += (s, e) => ct.Cancel(); try { var po = new ParallelOptions(); po.MaxDegreeOfParallelism = 64; po.CancellationToken = ct.Token; Parallel.For(1, 100, po, i => { po.CancellationToken.ThrowIfCancellationRequested(); DownloadInformation(i); }); } catch (Exception e) { Console.WriteLine(e); } finally { Console.ReadKey(); } }
Примечательно тут то, что нам пришлось пометить Main()
как async
– вот она, вирусность асинхронных вызовов!
А теперь о плохом. Если URL того файла что скачиваете не существует, происходит неизвестно_что
– иначе говоря, не выбрасывается никакого исключения, просто программа останавливается. И еще, Debug для меня не работает. Вообще. После первого же async
просто выбивает в caller, и все. На самом деле, в Async CTP описано еще несколько неприятных ограничений, но ведь это только CTP – можно пока не нервничать!
И что с того?
Итак, мы получили возможность “разгрузить” синтакс, и читабельность кода (имхо) возросла по сравнению даже с F#. Теперь можно описывать алгоритмы с продолжениями, хотя понятное дело, что для единичного вызова это особого смысла не имеет – нужно запускать несколько таких задач сразу, например через Parallel.For()
или можно просто создавать их, аггрегируя ссылки и потом делая Task.WaitAll()
.
С того момента как вышел Async CTP, мейлинг лист C# Insiders просто взорвался – все кому не лень играются с новым “китом” и примеряют его. Я пустил это дело “в продакшн” – поскольку риск минимален и все неплохо работает, я могу себе подобное позволить. Код выше уже отработал несколько часов, и претензий у меня не имеется. В прочему, тут и сложности-то кот наплакал.
Другие плюшки с PDC
Конечно, хотелось чтобы Андерс показал что-то умопомрачительное, но вместо этого меня даже как-то удивило то, что он, намекая на то что в C# будет метапрограммирование (а точнее не намекая – мы уже знаем что оно в какой-то форме будет) ответил на вопрос про АОП что он не считает правильным вклинивание в уже существующий код. Вопрос конечно же задал твиттер-аккаунт postsharp :) что естественно – PostSharp теперь платный, а C#5 рискует развалить им всю бизнес-модель. И развалит. Пускай C#5 не будет поддерживать вклинивание в уже существующие иерархии кода, но он даст нам создавать новый код. А нужен ли PostSharp когда я могу сам наладить инфраструктуру для внедрения в новые конструкты всего что мне нужно? Хмм, не уверен.
Еще один интеренсый момент – я послал на PDC вопрос на тему того почему Anders не использует ReSharper, и получил ответ что де “все знают что не стоит плагины ставить на неопробованную версию VS”. Но примечательно то, что пример у Андерса все равно накрылся. И вообще, если честно, я не понял смысл примера, в котором мы говорим студии выделять каждый if
-statement в регион. Разве ради этого компилятор переписывается? Или я невнимательно смотрел?
Ладно, поживем—увидим. ■
Оставить комментарий