Асинхронное программирование — цепочки вызовов
Когда в коде фигурирует пара вызовов
Итак, представьте что вы хотите в полностью асинхронном режиме скачать веб-страницу и сохранить ее у себя на жестком диске. Для этого нужно
Наивное решение задачи выглядит примерно вот так:
Это решение еще цветочки. Представьте, например, что вам дополнительно нужно из файла асинхронно скачать и сохранить все картинки, и только когда они все сохранены открыть папку в которую они были записаны. Используя парадигму выше, это настоящий кошмар.
Первое, что можно сделать – это сгруппировать кусочки функционала в анонимные делегаты[1]. Тогда получится примерно следующее:
Это решение выглядит хорошо, но если цепочка вызовов действительно большая, то код не будет читабельным, и им будет сложно управлять. Это особенно касается ситуаций когда, например, выполнение цепочки нужно приостановить и отменить.
Workflow – это конструкт F#. Идея примерно такая – вы определяете некий блок, в котором некоторые операторы (такие как
это значит что мы делаем вызов
Тем самым, аналог скачивания и записи файла на F# будет выглядеть примерно вот так:
Используя хитрые синтактические конструкции, F# позволяет нам с помощью специально созданных «примитивов» (таких как
Джефри Рихтер, всем известный автор книги CLR via C#, является также автором библиотеки PowerThreading. Эта библиотека[2] предоставляет ряд интересных фич, одна из которых – реализация аналога asynchronous workflow на C#.
Делается это очень просто – у нас появляется некий «менеджер токенов» под названием
Использовать
Далее, пишем код с использованием
Вот как выглядит наш метод скачивания при использовании
Как видите, асинхронный метод теперь записан в синхронном виде – мы даже умудрились использовать
Теперь осталось только вызвать этот метод:
Проблема цепочек асинхронных вызовов оказалась не такой страшной. Для простых ситуаций подойдут анонимные делегаты; для сложных есть асинхронные воркфлоу и
Цепочки – это просто, а что делать с целыми графами зависимостей? Об этом – в следующем посте. ■
BeginXxx()/EndXxx(), это приемлимо. Но что если алгоритм требует несколько таких вызовов подряд, то количество методов (или анонимных делегатов) преумножится и код станет менее читабельным. К счастью, эта проблема решена как в F# так и в C#.Задача
Итак, представьте что вы хотите в полностью асинхронном режиме скачать веб-страницу и сохранить ее у себя на жестком диске. Для этого нужно
- Начать скачивать страничку сайта
- Когда она скачается, начать запись файла на диск
- Когда запись завершилась, закрыть файловый поток и уведомить пользователя
Простое решение
Наивное решение задачи выглядит примерно вот так:
static void Main(string[] args)
{
Program p = new Program();
// начинаем загрузку
p.DownloadPage("http://habrahabr.ru");
// ждем 10сек.
p.waitHandle.WaitOne(10000);
}
// пришлось вывесить несколько переменных
private WebRequest wr;
private FileStream fs;
private AutoResetEvent waitHandle = new AutoResetEvent(false);
// тут мы начинаем скачивать страницу
public void DownloadPage(string url)
{
wr = WebRequest.Create(url);
wr.BeginGetResponse(AfterGotResponse, null);
}
// тут мы получаем текст со страницы
private void AfterGotResponse(IAsyncResult ar)
{
var resp = wr.EndGetResponse(ar);
var stream = resp.GetResponseStream();
var reader = new StreamReader(stream);
string html = reader.ReadToEnd();
// последний параметр true позволяет писать файлы асинхронно
fs = new FileStream(@"c:\temp\file.htm", FileMode.CreateNew,
FileAccess.Write, FileShare.None, 1024, true);
var bytes = Encoding.UTF8.GetBytes(html);
// начинаем запись файла
fs.BeginWrite(bytes, 0, bytes.Length, AfterDoneWriting, null);
}
// когда файл записан, устанавливаем wait handle
private void AfterDoneWriting(IAsyncResult ar)
{
fs.EndWrite(ar);
fs.Flush();
fs.Close();
waitHandle.Set();
}
Это решение еще цветочки. Представьте, например, что вам дополнительно нужно из файла асинхронно скачать и сохранить все картинки, и только когда они все сохранены открыть папку в которую они были записаны. Используя парадигму выше, это настоящий кошмар.
Решение через анонимные делегаты
Первое, что можно сделать – это сгруппировать кусочки функционала в анонимные делегаты[1]. Тогда получится примерно следующее:
private AutoResetEvent waitHandle = new AutoResetEvent(false);
public void DownloadPage(string url)
{
var wr = WebRequest.Create(url);
wr.BeginGetResponse(ar =>
{
var resp = wr.EndGetResponse(ar);
var stream = resp.GetResponseStream();
var reader = new StreamReader(stream);
string html = reader.ReadToEnd();
var fs = new FileStream(@"c:\temp\file.htm", FileMode.CreateNew,
FileAccess.Write, FileShare.None, 1024, true);
var bytes = Encoding.UTF8.GetBytes(html);
fs.BeginWrite(bytes, 0, bytes.Length, ar1 =>
{
fs.EndWrite(ar1);
fs.Flush();
fs.Close();
waitHandle.Set();
}, null);
}, null);
}
Это решение выглядит хорошо, но если цепочка вызовов действительно большая, то код не будет читабельным, и им будет сложно управлять. Это особенно касается ситуаций когда, например, выполнение цепочки нужно приостановить и отменить.
Решение с использованием asynchronous workflows
Workflow – это конструкт F#. Идея примерно такая – вы определяете некий блок, в котором некоторые операторы (такие как
let, например) переопределены. Asynchronous workflow – это такой workflow, внутри которого переопределены операторы (let!, do! и другие) так, что эти операторы позволяют «дождаться» завершения операции. То есть, когда мы пишемasync {
⋮
let! x = Y()
⋮
}
это значит что мы делаем вызов
BeginYyy() для Y, а когда результат доступен, записываем результат в x.Тем самым, аналог скачивания и записи файла на F# будет выглядеть примерно вот так:
// вот как надо строить пару начало/конец для асинхронных вызовов
// этот метод был по непонятным причинам убран из последних сборок F#
type WebRequest with
member x.GetResponseAsync() =
Async.BuildPrimitive(x.BeginGetResponse, x.EndGetResponse)
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://habrahabr.ru"))
Используя хитрые синтактические конструкции, F# позволяет нам с помощью специально созданных «примитивов» (таких как
GetResponseAsync() и AsyncWrite()) производить вызовы с Begin/End семантикой, но без разделения их на отдельные методы или делегаты. Как ни странно, примерно то же самое можно делать и в C#.Решение с использованием asynchronous enumerator
Джефри Рихтер, всем известный автор книги CLR via C#, является также автором библиотеки PowerThreading. Эта библиотека[2] предоставляет ряд интересных фич, одна из которых – реализация аналога asynchronous workflow на C#.
Делается это очень просто – у нас появляется некий «менеджер токенов» под названием
AsyncEnumerator. Этот класс фактически позволяет прерывать исполнение метода и продолжать его снова. Как можно прервать исполнение метода? Это делается с помощью нехитрого выражения yield return.Использовать
AsyncEnumerator просто. Берем и добавляем его как параметр в наш метод, а также меняем возвращаемое значение на IEnumerator<int>:public IEnumerator<int> DownloadPage(string url, AsyncEnumerator ae)
{
⋮
}
Далее, пишем код с использованием
BeginXxx()/EndXxx(), используя три простых правила:- Каждый BeginXxx() в качестве callback-параметра получает
ae.End() - Каждый EndXxx() в качестве токена
IAsyncResultполучаетae.DequeueAsyncResult() - Каждый раз когда нужно чего-то ждать, мы делаем
yield return X, где X – количество начатых операций
Вот как выглядит наш метод скачивания при использовании
AsyncEnumerator: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());
}
}
Как видите, асинхронный метод теперь записан в синхронном виде – мы даже умудрились использовать
using для файлового потока. Код стал более читабелен, если конечно не считать дополнительные вызовы yield return.Теперь осталось только вызвать этот метод:
static void Main()
{
Program p = new Program();
var ae = new AsyncEnumerator();
ae.Execute(p.DownloadPage("http://habrahabr.ru", ae));
}
Заключение
Проблема цепочек асинхронных вызовов оказалась не такой страшной. Для простых ситуаций подойдут анонимные делегаты; для сложных есть асинхронные воркфлоу и
AsyncEnumerator, в зависимости от того, какой язык вам ближе.Цепочки – это просто, а что делать с целыми графами зависимостей? Об этом – в следующем посте. ■
Заметки
- ↑ Примечательно то, что можно изначально сделать отдельные методы, а потом «заинлайнить» их с помощью ReSharper’а.
- ↑ Библиотека действительно интересная – советую открывать ее в Reflector’е, там много вкусного. Также, заметьте что лицензия библиотеки позволяет использовать ее только на Windows, что наверняка разозлит фанатов Mono



комментарии (15)