Async CTP: Async, Await и C#5

Думаю что было бы наивно не обсудить то, про что нам рассказали на последнем 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 в регион. Разве ради этого компилятор переписывается? Или я невнимательно смотрел?

Ладно, поживем—увидим. ■

5 responses to “Async CTP: Async, Await и C#5”

  1. Алексей Голиков Avatar
    Алексей Голиков

    Слава богу, блог не закрылся ;)

  2. async медленно и параллельно вырастал из разных мест – CCR, F#, AsyncEnumerator, Axum. Если посмотреть Чанел9, то видно, что конкретными пушерами и реализаторами async была группа программ-менеджеров из команды VB во взаимодействии с F#.

    Идея использовать енумераторы витала в блогах давно. Я помню не менее двух, помимо Рихтера и Chrysanthakopoulos’а. Но до реализации дошли AsyncEnumerator и CCR. Chrysanthakopoulos как-то сказал, что первое что он увидел в енумераторах это асинхронность. А Рихтер лицензировал асинхронный ридер-райтер лок.

    Вот такой экскурс в историю )

  3. […] Блог Д. Нестерука […]

  4. Michel Beloshitsky Avatar
    Michel Beloshitsky

    А конструкциями языка все это дело подхватывается? Если я напишу

    public class SmthEnum : IEnumerator
    {
     
        // ...
    
    
        public bool MoveNext()
        {
             // ...
        }
    
        public async Task Current
        {
             // ...
        }
    }

    то foreach это поймет?

Оставить комментарий