В прошлый раз мы говорили о том, как можно упростить работу с асинхронными операциями с помощью класса AsyncEnumerator Джеффри Рихтера, а также с помощью библиотеки Reactive Extensions. Однако, как поведал нам недавно Хейлсберг с компанией, счастье таки наступит с выходом новой версии языка C# и нам не понадобится использовать сторонние и не очень велосипеды, при работе с асинхронными операциями.
Идея, которая лежит в основе асинхронных операций в C# 5, очень похожа на ту, которую использовал Джеффри Рихтер в своем классе AsyncEnumerator, только на этот раз помимо нас с вами и старины Рихтера о ней еще узнал и компилятор (что здорово сказывается на вкусовых качествах сего синтаксического сахарка). Для начала, давайте вернемся к синхронной версии нашего гениального примера, который мы использовали ранее:
Здесь все очень просто (и главное весьма оригинально!): мы прикидываемся, что мы пишем что-то типа своего браузера, ну или просто нам делать нечего и нужно получить содержимое трех веб-страниц. Синхронная версия, как всегда замечательно работает, за исключением того, что мы ждем втрое больше времени, нежели могли бы, запустив эту операцию асинхронно. Поскольку, мы уже поняли, что это плохо и с удовольствием посмотрели выступление Хейлсберга, то попробуем прямо здесь, так же как и он, двумя росчерками мышки и тремя нажатиями на клаву, сделать из этого замечательного синхронного метода… асинхронный.
Первое, что нужно сделать, это изменить объявление нашей функции на следующее:
Ключевое слово async (которое в CTP версии является ключевым словом, а в релизе будет контекстным ключевым словом) в сигнатуре метода говорит о том, что этот метод выполняется асинхронно и возвращает управление вызывающему коду сразу после начала некоторой асинхронной операции. «Асинхронные методы» могут возвращать один из трех типов возвращаемого значения: void, Task и Task<T>. Если метод возвращает void, то это будет асинхронная операция типа: «запустили и забыли» («fire and forget»), поскольку обработать результат этой операции будет невозможно (хотя если не обработать исключения внутри этого метода, то грохнется он славно, и вполне может потянуть за собой и весь процесс!). В некоторых сценариях это бывает полезным (например, мы захотим асинхронно уведомить всех удаленных подписчиков и нам плевать на то, получат они эти сообщения или нет). С классами Task и Task<T> любознательный читатель (которому еще окончательно не надоели темпы, с которыми всеми нами любимая компания из Редмонда выпускает новые фичи), может быть знаком, поскольку они уже некоторое время живут и здравствуют в .Net Framework 4-й версии. Основная идея этих классов заключается в том, что они в себе инкапсулируют «незавершенную задачу» и мы можем дождаться ее завершения, установить «продолжение» (нечто, что должно быть вызвано при завершении этой задачи) и т.п. При этом класс Task<T> является наследником класса Task и отличается от последнего тем, что позволяет получить возвращаемое значение типа T посредством свойства Result, в то время, как класс Task скорее говорит о том, что некоторая задача возвращает void, и ценна за счет своих побочных эффектов.
Поскольку мы не просто хотим запустить на выполнение асинхронную операцию, но и получить результаты ее выполнения, но при этом нам важен не сам результат, а побочный эффект… В общем, мы используем тип Task в качестве возвращаемого значения (хотя могли бы спокойно использовать Task<string>, но, в общем, это не столь важно).
Изменив сигнатуру метода, нужно изменить немного и его тело. Для этого строки вида:
Нужно заменить на:
Ключевое слово await (в релизе это тоже будет контекстным ключевым словом) совсем не значит то, что мы залипнем в этой точке кода до тех пор, пока запрошенная асинхронная операция не будет выполнена. Это может сбивать с толку (и Эрик с компанией даже предлагают подыскать другие ключевые слова), но это означает в точности противоположную идею. Ключевое слово await означает, что после начала асинхронной операции (в данном случае после начала асинхронной операции получения WebResponse) метод должен вернуть управление, а продолжить его с этого же места уже после завершения асинхронной операции. Мудрыми словами это называется продолжениями и у Эрика есть с десяток статей, способных здорово вклинить мозги по этому поводу.
Если не вдаваться в матан, то в данном случае можно провести параллель с блоками итераторов в C# (если вы не знаком с этой темой, то я очень вам рекомендую это исправить), поскольку компилятор в обоих случаях генерирует конечный автомат, который отслеживает, где было прервано выполнение метода, и используется для корректного восстановления выполнения при последующем вызове. Однако, если в случае итераторов возобновление выполнения блока итераторов происходит при последующем вызове метода MoveNext внешним кодом, то асинхронный метод продолжит выполняться автоматом после завершения асинхронной операции. Для этого, в качестве «продолжения» текущей задачи устанавливается текущий метод, который и вызывается при завершении текущей задачи.
Итак, вот весь измененный метод целиком:
(Если честно, то я еще изменил вывод на консоль, чтобы было видно в каком потоке выполняется метод в данный момент, но это ведь наглядности ради, так что это изменение не в счет. Но в любом случае, старина Хейлсберг таки не соврал, изменений, действительно, минимальное количество).
И вот как этот метод вызывается:
А вот результат его выполнения:
А теперь давайте разберем подробно, что происходит внутри этого зверя. Итак, вызов метода AsyncVersion происходит в текущем потоке, и управление возвращается сразу же после первого оператора await. Но прежде чем вернуть управление вызывающему коду, метод AsyncVersion устанавливается в качестве «продолжения» текущей задачи и запоминается место, с которого нужно продолжить выполнение. Эта функция возвращает объект типа Task, для того, чтобы мы смогли дождаться завершения и проверить результаты. Затем, после завершения асинхронной операции, выполнение возобновляется с предыдущего места и, как мы видим, уже в другом потоке. Далее, этот процесс продолжается до тех пор, пока не будет завершена третья асинхронная операция, после чего метод task.Wait вернет управление, и мы увидим на консоли заветное: “Asynchronous task finished!”.
Обработка ошибок также претерпела некоторых изменений, но также весьма незначительных. Если вы уже знакомы с TPL, то узнаете знакомый класс System.AggregateExcpetion, который «собирает» все исключения и накапливает их у себя внутри. Причина этого заключается в том, что у одной задачи может быть десяток дочерних задач, каждая из которых может содержать еще несколько «подзадач», и каждое задание из этого «дерева» заданий может свалиться с собственным исключением. Именно для решения этой задачи и служит AggregateException, который содержит в себе «дерево» исключений, которое можно легко «выпрямить» с помощью метода Flatten (подробности можно почитать, например, здесь, раздел «Работа с AggregateException»). В общем, если обработка ошибок и усложнилась то не значительно, а сравнивая это с ночным кошмаром, с которым приходится сталкиваться при работе с APM, то такое «усложнение», даже проблемой назвать пальцы не повернутся.
На самом деле, этот пример не очень-то отличается от синхронного; ведь мы подряд выполняем три асинхронные операции, при этом каждая последующая операция начинается только после завершения предыдущей. Давайте попробуем переписать в новом виде наш второй пример, который одновременно получает результаты от трех веб-страниц и асинхронно записывает результат в файл:
Вот это уже действительно интересно! Мы получили полностью асинхронный код, но при этом он не выглядит так, будто над ним несколько ночей трудилось стадо индусов (а ведь асинхронный код подобной сложности именно так и выглядит). Он понятен и читается аналогично синхронному! (Если кто не верит, то пусть попробует переписать этот код в классическом APM стиле).
Итак, что мы имеем? Мы имеем штуку, которой действительно удобно пользоваться без боязни отстрелить себе ногу. Кроме того, даже не вдаваясь в серьезные дебри, более или менее понятно, как этим делом пользоваться (мда, хотя к именам придется привыкнуть). И самое интересное, что это далеко не вся функциональность. Я не рассказывал о контекстах синхронизации и о том, как это дело можно здорово использовать с потоком пользовательского интерфейса, о том, что такое TAP (Task-based Asynchronous Pattern), не говорил о том, что все новые асинхронные методы, которые я использовал являются методами расширения и вы можете их добавлять самостоятельно сколько угодно, да и не вдавался в дебри реализации… Но об этом всем, как-нибудь в другой раз! А сегодня, я вам предлагаю, просто скачать эту штуку и попробовать ее самостоятельно!
(Чуть не забыл, вот url, чтобы вы долго его не искали: msdn.com/vstudio/async)
Идея, которая лежит в основе асинхронных операций в C# 5, очень похожа на ту, которую использовал Джеффри Рихтер в своем классе AsyncEnumerator, только на этот раз помимо нас с вами и старины Рихтера о ней еще узнал и компилятор (что здорово сказывается на вкусовых качествах сего синтаксического сахарка). Для начала, давайте вернемся к синхронной версии нашего гениального примера, который мы использовали ранее:
- static void SyncVersion()
- {
- Stopwatch sw = Stopwatch.StartNew();
- string url1 = "http://rsdn.ru";
- string url2 = "http://gotdotnet.ru";
- string url3 = "http://blogs.msdn.com";
- var webRequest1 = WebRequest.Create(url1);
- var webResponse1 = webRequest1.GetResponse();
- Console.WriteLine("{0} : {1}, elapsed {2}ms", url1,
- webResponse1.ContentLength, sw.ElapsedMilliseconds);
-
- var webRequest2 = WebRequest.Create(url2);
- var webResponse2 = webRequest2.GetResponse();
- Console.WriteLine("{0} : {1}, elapsed {2}ms", url2,
- webResponse2.ContentLength, sw.ElapsedMilliseconds);
-
- var webRequest3 = WebRequest.Create(url3);
- var webResponse3 = webRequest3.GetResponse();
- Console.WriteLine("{0} : {1}, elapsed {2}ms", url3,
- webResponse3.ContentLength, sw.ElapsedMilliseconds);
- }
* This source code was highlighted with Source Code Highlighter.
Здесь все очень просто (и главное весьма оригинально!): мы прикидываемся, что мы пишем что-то типа своего браузера, ну или просто нам делать нечего и нужно получить содержимое трех веб-страниц. Синхронная версия, как всегда замечательно работает, за исключением того, что мы ждем втрое больше времени, нежели могли бы, запустив эту операцию асинхронно. Поскольку, мы уже поняли, что это плохо и с удовольствием посмотрели выступление Хейлсберга, то попробуем прямо здесь, так же как и он, двумя росчерками мышки и тремя нажатиями на клаву, сделать из этого замечательного синхронного метода… асинхронный.
Первое, что нужно сделать, это изменить объявление нашей функции на следующее:
- static async void AsyncVersion()
* This source code was highlighted with Source Code Highlighter.
Ключевое слово async (которое в CTP версии является ключевым словом, а в релизе будет контекстным ключевым словом) в сигнатуре метода говорит о том, что этот метод выполняется асинхронно и возвращает управление вызывающему коду сразу после начала некоторой асинхронной операции. «Асинхронные методы» могут возвращать один из трех типов возвращаемого значения: void, Task и Task<T>. Если метод возвращает void, то это будет асинхронная операция типа: «запустили и забыли» («fire and forget»), поскольку обработать результат этой операции будет невозможно (хотя если не обработать исключения внутри этого метода, то грохнется он славно, и вполне может потянуть за собой и весь процесс!). В некоторых сценариях это бывает полезным (например, мы захотим асинхронно уведомить всех удаленных подписчиков и нам плевать на то, получат они эти сообщения или нет). С классами Task и Task<T> любознательный читатель (которому еще окончательно не надоели темпы, с которыми всеми нами любимая компания из Редмонда выпускает новые фичи), может быть знаком, поскольку они уже некоторое время живут и здравствуют в .Net Framework 4-й версии. Основная идея этих классов заключается в том, что они в себе инкапсулируют «незавершенную задачу» и мы можем дождаться ее завершения, установить «продолжение» (нечто, что должно быть вызвано при завершении этой задачи) и т.п. При этом класс Task<T> является наследником класса Task и отличается от последнего тем, что позволяет получить возвращаемое значение типа T посредством свойства Result, в то время, как класс Task скорее говорит о том, что некоторая задача возвращает void, и ценна за счет своих побочных эффектов.
Поскольку мы не просто хотим запустить на выполнение асинхронную операцию, но и получить результаты ее выполнения, но при этом нам важен не сам результат, а побочный эффект… В общем, мы используем тип Task в качестве возвращаемого значения (хотя могли бы спокойно использовать Task<string>, но, в общем, это не столь важно).
Изменив сигнатуру метода, нужно изменить немного и его тело. Для этого строки вида:
- var webResponse1 = webRequest1.GetResponse();
* This source code was highlighted with Source Code Highlighter.
Нужно заменить на:
- var webResponse1 = await webRequest1.GetResponseAsync();
* This source code was highlighted with Source Code Highlighter.
Ключевое слово await (в релизе это тоже будет контекстным ключевым словом) совсем не значит то, что мы залипнем в этой точке кода до тех пор, пока запрошенная асинхронная операция не будет выполнена. Это может сбивать с толку (и Эрик с компанией даже предлагают подыскать другие ключевые слова), но это означает в точности противоположную идею. Ключевое слово await означает, что после начала асинхронной операции (в данном случае после начала асинхронной операции получения WebResponse) метод должен вернуть управление, а продолжить его с этого же места уже после завершения асинхронной операции. Мудрыми словами это называется продолжениями и у Эрика есть с десяток статей, способных здорово вклинить мозги по этому поводу.
Если не вдаваться в матан, то в данном случае можно провести параллель с блоками итераторов в C# (если вы не знаком с этой темой, то я очень вам рекомендую это исправить), поскольку компилятор в обоих случаях генерирует конечный автомат, который отслеживает, где было прервано выполнение метода, и используется для корректного восстановления выполнения при последующем вызове. Однако, если в случае итераторов возобновление выполнения блока итераторов происходит при последующем вызове метода MoveNext внешним кодом, то асинхронный метод продолжит выполняться автоматом после завершения асинхронной операции. Для этого, в качестве «продолжения» текущей задачи устанавливается текущий метод, который и вызывается при завершении текущей задачи.
Итак, вот весь измененный метод целиком:
- static async Task AsyncVersion()
- {
- Stopwatch sw = Stopwatch.StartNew();
- string url1 = "http://rsdn.ru1";
- string url2 = "http://gotdotnet.ru";
- string url3 = "http://blogs.msdn.com";
-
- var webRequest1 = WebRequest.Create(url1);
- Console.WriteLine("Before webRequest1.GetResponseAsync(). Thread Id: {0}",
- Thread.CurrentThread.ManagedThreadId);
-
- var webResponse1 = await webRequest1.GetResponseAsync();
- Console.WriteLine("{0} : {1}, elapsed {2}ms. Thread Id: {3}", url1,
- webResponse1.ContentLength, sw.ElapsedMilliseconds,
- Thread.CurrentThread.ManagedThreadId);
-
- var webRequest2 = WebRequest.Create(url2);
- Console.WriteLine("Before webRequest2.GetResponseAsync(). Thread Id: {0}",
- Thread.CurrentThread.ManagedThreadId);
-
- var webResponse2 = await webRequest2.GetResponseAsync();
- Console.WriteLine("{0} : {1}, elapsed {2}ms. Thread Id: {3}", url2,
- webResponse2.ContentLength, sw.ElapsedMilliseconds,
- Thread.CurrentThread.ManagedThreadId);
-
- var webRequest3 = WebRequest.Create(url3);
- Console.WriteLine("Before webRequest3.GetResponseAsync(). Thread Id: {0}",
- Thread.CurrentThread.ManagedThreadId);
- var webResponse3 = await webRequest3.GetResponseAsync();
- Console.WriteLine("{0} : {1}, elapsed {2}ms. Thread Id: {3}", url3,
- webResponse3.ContentLength, sw.ElapsedMilliseconds,
- Thread.CurrentThread.ManagedThreadId);
- }
* This source code was highlighted with Source Code Highlighter.
(Если честно, то я еще изменил вывод на консоль, чтобы было видно в каком потоке выполняется метод в данный момент, но это ведь наглядности ради, так что это изменение не в счет. Но в любом случае, старина Хейлсберг таки не соврал, изменений, действительно, минимальное количество).
И вот как этот метод вызывается:
- static void Main(string[] args)
- {
-
- try
- {
- Console.WriteLine("Main thread id: {0}", Thread.CurrentThread.ManagedThreadId);
- var task = AsyncVersion();
- Console.WriteLine("Right after AsyncVersion() method call");
- //Ожидаем завершения асинхронной операции
- task.Wait();
- Console.WriteLine("Asyncronous task finished!");
-
- }
- catch(System.AggregateException e)
- {
- //Все исключения в TPL пробрасываются обернутые в AggregateException
- Console.WriteLine("AggregateException: {0}", e.InnerException.Message);
- }
- Console.ReadLine();
- }
* This source code was highlighted with Source Code Highlighter.
А вот результат его выполнения:
Main thread id: 10
Before webRequest1.GetResponseAsync(). Thread Id: 10
Right after AsyncVersion() method call
rsdn.ru: 1672, elapsed 657ms. Thread Id: 13
Before webRequest2.GetResponseAsync(). Thread Id: 13
gotdotnet.ru: 99470, elapsed 1915ms. Thread Id: 14
Before webRequest3.GetResponseAsync(). Thread Id: 14
blogs.msdn.com: 47927, elapsed 2628ms. Thread Id: 15
Asynchronous task finished!
А теперь давайте разберем подробно, что происходит внутри этого зверя. Итак, вызов метода AsyncVersion происходит в текущем потоке, и управление возвращается сразу же после первого оператора await. Но прежде чем вернуть управление вызывающему коду, метод AsyncVersion устанавливается в качестве «продолжения» текущей задачи и запоминается место, с которого нужно продолжить выполнение. Эта функция возвращает объект типа Task, для того, чтобы мы смогли дождаться завершения и проверить результаты. Затем, после завершения асинхронной операции, выполнение возобновляется с предыдущего места и, как мы видим, уже в другом потоке. Далее, этот процесс продолжается до тех пор, пока не будет завершена третья асинхронная операция, после чего метод task.Wait вернет управление, и мы увидим на консоли заветное: “Asynchronous task finished!”.
Обработка ошибок также претерпела некоторых изменений, но также весьма незначительных. Если вы уже знакомы с TPL, то узнаете знакомый класс System.AggregateExcpetion, который «собирает» все исключения и накапливает их у себя внутри. Причина этого заключается в том, что у одной задачи может быть десяток дочерних задач, каждая из которых может содержать еще несколько «подзадач», и каждое задание из этого «дерева» заданий может свалиться с собственным исключением. Именно для решения этой задачи и служит AggregateException, который содержит в себе «дерево» исключений, которое можно легко «выпрямить» с помощью метода Flatten (подробности можно почитать, например, здесь, раздел «Работа с AggregateException»). В общем, если обработка ошибок и усложнилась то не значительно, а сравнивая это с ночным кошмаром, с которым приходится сталкиваться при работе с APM, то такое «усложнение», даже проблемой назвать пальцы не повернутся.
На самом деле, этот пример не очень-то отличается от синхронного; ведь мы подряд выполняем три асинхронные операции, при этом каждая последующая операция начинается только после завершения предыдущей. Давайте попробуем переписать в новом виде наш второй пример, который одновременно получает результаты от трех веб-страниц и асинхронно записывает результат в файл:
- public static async void AsyncVersion2()
- {
- Stopwatch sw = Stopwatch.StartNew();
- var urls = new string[] {"http://rsdn.ru", "http://gotdotnet.ru",
- "http://blogs.msdn.com"};
- var tasks = (from url in urls
- let webRequest = WebRequest.Create(url)
- select new {Url = url, Response = webRequest.GetResponseAsync()})
- .ToList();
- var data = await TaskEx.WhenAll(tasks.Select(t=>t.Response));
- var sb = new StringBuilder();
- foreach(var s in tasks)
- {
- sb.AppendFormat("{0}: {1}, elapsed {2}ms. Thread Id: {3}", s.Url,
- s.Response.Result.ContentLength,
- sw.ElapsedMilliseconds, Thread.CurrentThread.ManagedThreadId)
- .AppendLine();
- }
- var outputText = sb.ToString();
- Console.WriteLine("Web request results: {0}", outputText);
-
- using (var fs = new FileStream("d:\\results.txt", FileMode.Create,
- FileAccess.Write, FileShare.Write))
- {
- await
- fs.WriteAsync(UnicodeEncoding.Default.GetBytes(outputText), 0,
- UnicodeEncoding.Default.GetByteCount(outputText));
- }
-
- }
* This source code was highlighted with Source Code Highlighter.
Вот это уже действительно интересно! Мы получили полностью асинхронный код, но при этом он не выглядит так, будто над ним несколько ночей трудилось стадо индусов (а ведь асинхронный код подобной сложности именно так и выглядит). Он понятен и читается аналогично синхронному! (Если кто не верит, то пусть попробует переписать этот код в классическом APM стиле).
Итак, что мы имеем? Мы имеем штуку, которой действительно удобно пользоваться без боязни отстрелить себе ногу. Кроме того, даже не вдаваясь в серьезные дебри, более или менее понятно, как этим делом пользоваться (мда, хотя к именам придется привыкнуть). И самое интересное, что это далеко не вся функциональность. Я не рассказывал о контекстах синхронизации и о том, как это дело можно здорово использовать с потоком пользовательского интерфейса, о том, что такое TAP (Task-based Asynchronous Pattern), не говорил о том, что все новые асинхронные методы, которые я использовал являются методами расширения и вы можете их добавлять самостоятельно сколько угодно, да и не вдавался в дебри реализации… Но об этом всем, как-нибудь в другой раз! А сегодня, я вам предлагаю, просто скачать эту штуку и попробовать ее самостоятельно!
(Чуть не забыл, вот url, чтобы вы долго его не искали: msdn.com/vstudio/async)