C# 5 — об async/await с начала

В недавно вышедшей Visual Studio 11 Beta встроена новая и главная фишка будущего C# 5 — асинхронное программирование с помощью async/await. Про нее уже написано достаточно много статей в том, числе на хабре — например, эта серия статей. Однако, я для себя так и не понял в чем суть нового синтаксиса, пока сам не попробовал его в деле. Данная статья — попытка самому структурировать и до конца разобраться с этим достаточно интересным инструментом и поделиться результатами с сообществом, рассказав про него немного иначе. Итак, поехали…

Зачем это нужно?


C# активно развивающийся язык в каждой версии которого появляются интересные особенности, которые действительно упрощают написание качественного и понятного кода. Например, Linq значительно упростил написание фильтров для коллекций и sql-запросов в коде. Сейчас дошла очередь до асинхронности.

И хотя во фреймворке уже накопилось достаточно большое количество способов написания многопоточного кода, от ручного создания потока до визуального компонента BackgroundWorker, разработка асинхронного приложения требует написания приличного объема инфраструктурного кода, который увеличивается по мере усложнения задачи. Главной проблемой этого, на мой взгляд, является необходимость изменения состояния пользовательского интерфейса из фонового потока (чего, как известно, нельзя делать напрямую).

Новая языковая конструкция async/await решает эту проблему, позволяя не только запускать задачу в фоновом потоке и при ее завершении выполнить код в основном, но и делает это наглядно — код выглядит почти как синхронный (включая обработку исключений).

Встречаем: async/await


Вообще говоря ключевые слова не достаточно ясно отражают суть, но как объясняют разработчики лучше никто не придумал, поэтому они такие какие есть. Как и большинство сущностей в программировании, новую конструкцию проще всего понять непосредственно из кода:

// Синхронная версия
private void OnButtonClick()
{
   TextBox.Text = new WebClient().DownloadString("http://habrahabr.ru/");  
}

// Асинхронная версия
private async void OnButtonClick()
{
   TextBox.Text = await new WebClient().DownloadStringTaskAsync("http://habrahabr.ru/");
}


И если в синхронном варианте все просто и понятно, то с асинхронным возникает много вопросов. Начнем как ни странно с нового метода у класса WebClient — DownloadStringTaskAsync — этот метод возвращает Task<string> и в новой студии отмечается как awaitable. Тип возвращаемого значения ключевой момент во всей этой истории — забегая вперед стоит сказать, что await умеет работать только с функциями возвращающими Task и Task<T>.
UPD: как справедливо заметил jack128 в комментариях, await работает с любым объектом, у которого есть метод GetAwaiter(), спасибо ему за корректировку.

Итак, метод DownloadStringTaskAsync создает задачу Task и сразу возвращает ее из функции, в то время как в фоновом потоке начинает скачиваться страница с запрошенного url. Мы вольны работать непосредственно с объектом Task вручную дождавшись выполнения результата:

private void OnButtonClick()
{
  Task<string> task = new WebClient().DownloadStringTaskAsync("http://microsoft.com/");
  task.Wait(); // Здесь мы ждем завершения задачи, что блокирует поток
  TextBox.Text = task.Result;
}


Данный код разумеется остается синхронным, так как мы в основном потоке ждем выполнения фонового…

Нужен способ как удобно и асинхронно работать с задачей (Task<T>), которая осталась единственной «ниточкой», которая связывает нас с фоновым потоком. И здесь на сцене появляется await — он не только разворачивает Task<T> в T, но и устанавливает остаток метода в «продолжение» (continuation), которое выполнится после завершения задачи и самое главное в том же потоке. При этом произойдет выход из функции OnButtonClick() и приложение продолжит работать в штатном режиме — реагируя на действия пользователей.

Как только фоновый поток завершит работу и вернет результат — будет выполнено «продолжение» в основном потоке, которое в данном случае установит содержимое страницы в текстовое поле.

Осталось разобраться с ключевым словом async — им необходимо, помечать те функции в которых будет использоваться await. Все просто, а главное компилятор присмотрит, чтобы вы не забыли про это — не дав скомпилировать программу.

Как это выглядит в действии


В функции может быть несколько await'ов, что позволяет создавать асинхронные цепочки выполнения:

private async void StartButtonClick(object sender, RoutedEventArgs e)
{
   // Убираем возможность повторного нажатия на кнопку
   StartButton.IsEnabled = false;

   // Вызываем новую задачу, на этом выполнение функции закончится
   // а остаток функции установится в продолжение
   TextBox.Text = await new WebClient().DownloadStringTaskAsync("http://habrahabr.ru/");
   StatusLabel.Content = "Загрузка страницы завершена, начинается обработка";

   // В продолжении можно также запускать асинхронные операции со своим продолжением
   var result = await Task<string>.Factory.StartNew(() =>
   {        
     Thread.Sleep(5000); // Имитация длительной обработки...
     return "Результат обработки";
   });

   // Продолжение второй асинхронной операции
   StatusLabel.Content = result;
   StartButton.IsEnabled = true;
}


А что на счет исключений? Раз уж разработчики пообещали, что новый синтаксис будет простым и близким к синхронному, они не могли обойти стороной такую важную проблему обработки исключений. Как и обещалось, исключения брошенные в фоновом потоке, можно обработать используя классический синтаксис (как будто никакой асинхронности и нет):

private async void StartButtonClick(object sender, RoutedEventArgs e)
{
   try
   {
     TextBox.Text = await new WebClient().DownloadStringTaskAsync("http://not-habrahabr.ru/");
   }
   catch (Exception ex)
   {
     MessageBox.Show(ex.Message);
   }
}


Однако, с обработкой исключений есть один момент, который нужно понимать — так как весь код, идущий после await устанавливается в завершение и когда он будет выполнен вообще неизвестно, то такая обработка исключений не будет работать:

// Это не работает!!!
private void StartButtonClick(object sender, RoutedEventArgs e)
{
   try
   {     
     Download();     
   }
   catch (Exception ex)
   {
     MessageBox.Show(ex.Message);
   }
}

private async void Download()
{
   TextBox.Text = await new WebClient().DownloadStringTaskAsync("http://not-habrahabr.ru/");
}



Функция Download вернет управление как только будет создан Task с фоновым потоком, а после этого будет выполнен и выход из функции StartButtonClick… и уже позже в фоновом потоке будет сгенерировано исключение о том, что не удается разрешить доменное имя. Более подробное объяснение можно почитать здесь.

Итого


В грядущем .Net 4.5 многие классы, будет дополнены для поддержки нового синтаксиса — т.е. появится много функций возвращающих Task и Task<T>. И судя по простоте нового синтаксиса он получит большое распространение, поэтому необходимо ясное понимание новых конструкций языка, их действия и области применения.

Подытожим — ключевое слово async не приводит к тому, что метод будет выполняться в фоновом потоке (как кажется из названия), а только отмечает, что внутри метода присутствует await, который работает с Task и Task<T> таким образом, что остаток метода после await будет выполнен после завершения Task, но в основном потоке.

Вводя новый синтаксис, компания Microsoft предлагает не просто синтаксический сахар, но целый паттерн асинхронного программирования, включающий и обработку исключений, и метод прерывания выполнения асинхронной функции, и информирование о прогрессе выполнения… которые уже выходят за рамки данной статьи.

Если остались какие-либо туманные места, постараюсь в комментариях ответить на все вопросы.
Поделиться публикацией

Похожие публикации

Комментарии 62

    +2
    А откуда именно потом будет вызвано continuation? Из главного event loop-а?
      +2
      ну будет наверно какой-нить Dispatcher как в WPF, который будет в event loop вызываться.
        0
        Да я просто думаю, делать логику сервачка по-старинке через yield или таки попробовать использовать async. С yield не будет лишних потоков, а вот как работает с сокетами async, пока не ясно.
          +1
          Насколько я понимаю, вся эта эпопея с await затеяна именно для облегчения интеграции многопоточности с UI. В случае с программой без него, не будет никакого профита от продолжений, которые выполнятся в случайном потоке из пула, только накладные расходы.
        +1
        Из System.Threading.SynchronizationContext.Current
          +1
          Отлично, а его кто пинает? Ну не может быть такого, чтобы посреди выполнения кода (который вполне может быть внутри какой-нибудь блокировки) его прерывали и переключали контекст на continuation.
            +2
            Если я все правильно помню, для не UI кода по умолчанию это все реализовано через ThreadPool и егошний диспетчер, т.е. никакой код не прерывается, просто берется свободный поток из пула или создается новый. В случае с UI — используется Dispatcher UI фреймворка (WinForms, WPF, SL), чтобы обеспечить выполнения продолжения в контексте UI потока.
              0
              Так оно в итоге только для UI-потоков гарантирует, что continuation будет вызван в том же потоке, в котором вызвали метод через await?
                +1
                Не совсем. По умолчанию для не UI потоков не задан какой-то специфичный SynchronizationContext, поэтому выполнение идет через TaskScheduler.Current, который просто раскидывает задачи по потокам из пула. Но у вас всегда есть возможность задать свой SynchronizationContext, в котором определить требуемое поведение.
        +2
        В С++11 появилась схожая штука: ::std::async.
        Передав в нее функцию, на выходе получаем ::std::future (continuation вообщем-то).
        Пока результат нам не нужен — делаем посторонние задачи. Как нужен — вызываем метод get() у ::std::future, который или сразу вернет результат (если он уже посчитан) или заблокируется пока результата не будет.

        К чему я это — к тому что асинхронность добавляется в разные языки, а значит набирает популярность у неискушенных программистов.
          +3
          Аналог того, что вы описали, было в C# задолго до async/await.
            0
            А в чем тут отличие? Я с Си-шарпом только в универе был знаком.
              +2
              Ну была штука под называнием IAsyncResult, которую возвращали асинхронные методы. Очень давно была.
                0
                Любые методы или какие-то специально написанные? Если любые — так это понятно, это особенность конкретной библиотеки. А ::std::async это особенность языка — он принимает любую синхронную функцию, кладет ее в очередь, занимается пулом потоков — это все теперь проблемы языка и реализации стандартной библиотеки.
                  0
                  Тьфу, наоборот
                  >> Если любые — так это понятно
                  Следует понимать как
                  >> Если специально написанные — так это понятно.

                  Да, я посмотрел IAsyncResult ну это просто стандартный интерфейс под хэндл, а не механизм же.
                    0
                    Можно брать _любую_ функцию и вызывать ее через делегат
                      0
                      Ок. Посмотрю еще. А чем отличается от нововведения, описанного в статье?
                        0
                        Там просто запуск кода в фоне с возможностью потом его синхронно подождать. Описанное в статье возвращает выполнение в вызывающий код сразу, а уже потом из event-loop-а будет вызван обработчик результата, причём сам код пишется так, как будто он синхронный.
                          0
                          Ну вот вы писали "… который или сразу вернет результат (если он уже посчитан) или заблокируется пока результата не будет." Работа с IAsyncResult примерно такая же (хотя есть дополнительные плюшки). Плюсы async/await как в упрощенном синтаксисе (код выглядит как синхронный), так и в отсутствии необходимости самому опрашивать возвращенный «специальный» объект на наличие результата. Ничего блокироваться не будет.
                        0
                        Это часть того, что называется в дотнете APM — asynchronous programming model. Классы, которые поддерживают асинхронное выполнение, должны следовать рекомендациям по их оформлению соответствующим образом. К примеру, если обычный метод назывался DoSomething, то его асинхронная версия должна быть парой методов BeginDoSomething/EndDoSomething. Begin-метод должен возвращать IAsyncResult, а End-метод принимать его в качестве аргумента.
                        А IAsyncResult вы можете реализовать как вы хотите (если вы делаете асинхронный API). Это не обязательно просто хранилище для хендла, в общем случае это объект, представляющий собой контекст выполняемой асинхронной операции.
                +3
                >В С++11 появилась схожая штука: ::std::async.
                ничего общего с асинками в C# 5
                автор шарповых асинков уже работает над тем же самым для будущего C++
                  0
                  Да, мне уже объяснили.

                  Ох, «будущий С++». Саттер сказал, что еще года 3 книг по С++11-то не появится, так как мало опыта. Так что «будущий С++» будет лет через 10. Если будет, конечно…
                    +1
                    >Саттер сказал, что еще года 3 книг по С++11-то не появится, так как мало опыта.
                    появятся, на GoingNative у кого-то на слайде были примерные даты по выходу новых изданий известных книг по Си++.

                    >Так что «будущий С++» будет лет через 10
                    Последнее время к Си++ снова возрастает внимание, а вместе с ним и финансовые вливания. Так что думаю что будущий Си++ не придётся ждать так долго.

                    А если хочется аналога шарповых асинков прямо сейчас, то можно с помощью небольшой магии с макросами сделать примерно тоже самое, все детали по асинкам разжованы в блогах майкрософтовцев.
                      0
                      Вы ведь смотрели GoingNative?
                      Там фраза и про стандарт была, что еще даже загадывать рано и сейчас основная проблема — библиотеки. Нужно хотя бы лет за 5 набрать объем сопоставимый с с# и java.

                      Про книги я оттуда же и взял: и серьезные книги (типа Exceptional C++ или Effective C++) это как раз через 3 года минимум.
                      0
                      Он недавно писал что под «y» в новом стандарте C++1y они надеются что будет 7.
                      herbsutter.com/2012/03/08/trip-report-february-2012-c-standards-meeting/
                        0
                        Если C++0x планировали «ну точно» доделать до 2009-го, а потратили восемь лет, при этом выкинув часть планируемых фич, то шансы, что уложатся в шесть лет, я бы сказал, невелики. :)
                  +1
                  Выглядит действительно вкусно. Жаль, нескоро это появится у большинства пользователей. У многих не то что 4.5, даже 4.0 то не стоит )
                    –1
                    Windows 8 думаю с 4.5 выпустят, или только 4.0 там будет?
                      +2
                      4.5 я надеюсь)
                      Однако, учитывая крупные изменения в Win8, далеко не факт, что народ туда быстро перелезет.
                      У XP то все еще довольно весомая доля рынка…
                        0
                        Ну накатят Service Pack обязательный на XP и семерку, и там 4.5 присунут.
                        Хотя вроде дальше SP3 на XP сказали что не будут делать.
                          +5
                          Не накатят) На XP точно нет.
                        +3
                        выпустят вместе с 4.5
                        пруф
                        +1
                        А в чём проблема таскать фреймворк с собой?
                        • НЛО прилетело и опубликовало эту надпись здесь
                            +1
                            Плюс ко всему можно даже не таскать с собой, а в инсталлере проверять его наличие, и грузить в процессе установки, при необходимости.
                              +1
                              Так и делаю. Проблема в том, что у некоторых юзеров возникают проблемы с установленным ранее .NET. Не работает программа, пока не переустановишь его с нуля. Правда, к наличию изначально у юзера .NET это отношения не имеет, согласен.
                                0
                                А можно поподробнее?
                                  0
                                  Возникают совершенно безумные эксцепшны при инициализации главного окна программы.
                                  Что то вроде:
                                  Исключение 0: Инициализатор типа «System.Windows.Media.FontFamily» выдал исключение.
                                  0: — Void System.Windows.Media.Typeface..ctor(System.Windows.Media.FontFamily, System.Windows.FontStyle, System.Windows.FontWeight, System.Windows.FontStretch)
                                  0: — System.Windows.Media.Typeface MS.Internal.Text.DynamicPropertyReader.GetTypeface(System.Windows.DependencyObject)
                                  0: — Void MS.Internal.Text.TextProperties.InitCommon(System.Windows.DependencyObject)
                                  0: — Void MS.Internal.Text.TextProperties..ctor(System.Windows.FrameworkElement, Boolean)

                                  с внутренним эксцепшном

                                  Исключение 1: Исключение из HRESULT: 0xD000009C
                                  0: — Void System.Runtime.InteropServices.Marshal.ThrowExceptionForHRInternal(Int32, IntPtr)
                                  0: — Void System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(Int32, IntPtr)
                                  0: — Void MS.Internal.Text.TextInterface.Native.Util.ConvertHresultToException(Int32)
                                  0: — MS.Internal.Text.TextInterface.FontCollection MS.Internal.Text.TextInterface.Factory.GetSystemFontCollection(Boolean)
                                  0: — MS.Internal.Text.TextInterface.FontCollection MS.Internal.FontCache.DWriteFactory.get_SystemFontCollection()
                                  0: — Void System.Windows.Media.FontFamily..cctor()

                                  или какой то компонент не находится. или просто пишет что не удалось проинициализировать программу корректно.

                                  Использую .NET 4.0 и WPF
                                    0
                                    Эксепшены всегда связаны со шрифтами?
                                      0
                                      Нее… Иногда не может найти System.Web, это я так понимаю если Compact Framework стоит. Иногда еще чего не может найти. Иногда в XAML ему что-то не нравится. В общем, странные эксцепшны.
                                        0
                                        Первый раз слышу вообще о таких багах, может на целевых компьютерах файлы .net повреждены?
                                          0
                                          Ну так я и написал) Скорее всего у них он поврежден. Поэтому переустановка помогает в 99% случаев. В оставшемся 1% видимо вся система через жопу установлена или загажена до такой степени.
                                            0
                                            Возможно, у них просто стоит ClientProfile? Или как вариант, не накачены патчи, которые вам нужны?
                                              0
                                              ClientProfile объясняет только некоторые из эксцепшнов)
                                              Патчей никаких мне не надо)
                          –1
                          BackgoundWorker можно закапывать?
                            –1
                            BackgroundWorker для тасков с апдейтом прогресса нужен будет.
                            –9
                            Ух ты, монада!
                              +1
                              Как только фоновый поток завершит работу и вернет результат — будет выполнено «продолжение» в основном потоке — могу сильно ошибаться, но вроде как это не всегда так. Т.е. «продолжение» может быть вызвано в совсем другом потоке нежели начало метода. Все зависит от того, как настроен текущий Dispatcher. В случае с WPF/Winforms приложений Dispatcher всегда вызывает код таски и ее продолжения в UI потоке. В общем случае это не так.

                              Кстати, это может привести к очень трудноуловимым багам. Представьте следующий код:
                              lock(syncRoot)
                              {
                              await Download();
                              }

                              Да, он не очень логичен и даже не скомпилируется (внутри lock и catch await делать нельзя).
                              Беда в том, что многие текущие синхропримитивы привязаны к тому, что код внутри них выполняется в одном потоке, а с тасками это не всегда так.
                                +1
                                Совершенно верно, все зависит от текущего диспатчера. Вообще не вижу особого смысла применять эту технику для не UI потока, если только ради более красивой обработки исключений )
                                  +6
                                  Для не UI потока это точно так же полезно, поскольку код становится проще и понятней. Попробуйте переписать любым другим образом код, который будет содержать 3-4 вызова await. Там такая каша получится, будь здоров.

                                  Еще один плюс await-ов в том, что они делают один лексический скоуп, что позволяет пользоваться такими вещами, как using, try/finally и т.д.

                                  Т.е. это не просто более красивый код обработки исключений, эти штуки дают более читабельный код, который можно спокойно читать без боязни заработать непоправимые заболевания от вывиха извилин.
                                    +1
                                    Мне несколько раз приходилось писать туже самую state машину, что генерируется await'ами, но на ManualResetEvent'ах — выглядело ужасно. С await'ами же все это укладывается в несколько строчек.
                                  –5
                                  Встречайте, FCC :D
                                  А unwind-protect не асилили. Слабаки
                                    +1
                                    Красиво и понятно. Спасибо за объяснение. Жду с нетерпением!
                                      0
                                      Когда сложное объясняется просто :) Спасибо!
                                        –1
                                        А await коллекции возможен? Например, запустить обсчет AI четырех персонажей (соответственно в четырех потоках), а когда все расчеты закончатся — что-то продолжить делать.
                                          +2
                                          Это через Parallel Extensions делается вообще говоря.
                                          0
                                          В общем виде это будет выглядеть как-то так:

                                              List<Person> persons;
                                              ...
                                              await TaskEx.WhenAll(from p in persons select p.ProcessAsync());
                                              ...
                                              продолжение
                                          


                                          Вот здесь в видео примерах есть применение WhenAll
                                            0
                                            — забегая вперед стоит сказать, что await умеет работать только с функциями возвращающими Task и Task.

                                            Ты ошибаешься. await будет работать с любым типом, имеющим метод(или экстеншн метод) GetAwaiter()
                                            Ну короче тоже самое, как и foreach, которому совсем не нужен IEnumerable. Достаточно того, чтоб тип имел метод GetEnumerator. Или как query syntax в линке. Реализуй для int'а экстеншн методы Select/Where/SelectMany и пиши спокойно:
                                            var _ = from x in 10
                                            from y in 20
                                            where y > 15
                                            select x + y;
                                              0
                                              с таким косяком «особенностью» в Exception Handling — этот концепт становится алогичным и неприменимым.
                                                0
                                                Именно поэтому методы имеющие в теле await отмечаются как async. Нужно просто правильно использовать технологию… асинхронность как ни круто будет накладывать ограничения. Новый синтаксис сильно сокращает количество инфраструктурного кода + облегчает обработку исключений, но нельзя же ждать чтобы код вел себя совсем уж как синхронный
                                                0
                                                Task<string> task = new WebClient().DownloadStringTaskAsync("http://microsoft.com/");
                                                task.Wait(); // Здесь мы ждем завершения задачи, что блокирует поток
                                                TextBox.Text = task.Result;
                                                

                                                Конечно, посту уже 100 лет. Но вот что-то мне кажется, что Вы не проверяли это.
                                                Такая штука работать не будет)
                                                  0
                                                  Почему?
                                                  Все отлично работает.

                                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                Самое читаемое