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

в 10:35, , рубрики: async, async pattern, 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/");
}

* This source code was highlighted with Source Code Highlighter.

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

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

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

* This source code was highlighted with Source Code Highlighter.

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

Нужен способ как удобно и асинхронно работать с задачей (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;
}

* This source code was highlighted with Source Code Highlighter.

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

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);
   }
}

* This source code was highlighted with Source Code Highlighter.

Однако, с обработкой исключений есть один момент, который нужно понимать — так как весь код, идущий после 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/");
}

* This source code was highlighted with Source Code Highlighter.

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

Итого

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

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

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

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

Автор: justserega

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js