вторник, 29 ноября 2011 г.

Я.Субботник в Санкт-Петербурге, 3 декабря

Я.Субботник в Санкт-Петербурге пройдет 3 декабря в офисе Яндекса.

Регистрация на мероприятие начнется 23 ноября. Количество мест ограничено.

Для тех, кто не попадёт в число участников или не сможет лично присутствовать на Я.Субботнике, будет организована онлайн-трансляция.

Подробную информацию о мероприятии читайте здесь.

воскресенье, 31 октября 2010 г.

Асинхронное программирование и C# 5.0

На PDC 2010 были анонсированы асинхронные функции в C# и VB.NET. Рассказ о них хочется начать историей эволюции C# Эрика Липперта:

Проектировщики С# 2.0 осознали мучительность процесса написания логики работы итератора. Так были введены блоки итераторов (iterator blocks). Это был способ дать понять компилятору как построить конечный автомат для хранения продолжения (continuation) - “что будет дальше” – в состоянии где-то внутри, скрытом за кулисами, так чтобы больше не приходилось описывать это в коде.

Заодно они поняли трудность написания небольших методов, использующих локальные переменные и добавили анонимные методы (anonymous methods), чтобы дать возможность компилятору понять как обернуть локальные переменные в замыкающий класс и избавить вас от написания этого кода.

Проектировщики С# 3.0 поняли сложность написания кода, выполняющего сортировку, фильтрацию, объединение, группирование или обобщение сложных данных, поэтому и были добавлены выражения запроса (query comprehensions) и все остальные возможности LINQ. Тем самым компилятор научился делать правильные вызовы объектной модели для построения запроса, деревьев выражений и всего остального.

Проектировщики С# 4.0 поняли трудность взаимодействия как с современной, так и со старой динамической объектной моделью, поэтому и был добавлен тип dynamic. Так компилятор научился генерировать во время компиляции код, который будет обрабатываться в Dynamic Language Runtimeво время выполнения.

Проектировщики C# 5.0 поняли сложность написания асинхронного кода: его трудно понять, преобразование в continuation сложно и насыщает код вспомогательными механизмами, делающими неясным его назначение.

Так больше не могло продолжаться.


Итак, что же мы получили и зачем?

Microsoft Visual Studio Async CTP представила новые языковые возможности для C# и VB и новые паттерны, позволяющие выполнять асинхронное программирование подобно и почти так же прямолинейно, как и синхронное.

Здесь и дальше под асинхронным программированием (asynchronous programming) я буду понимать асинхронную концепцию (стиль) программирования, основанную на том, что результат выполнения функции (операции), может быть доступен не сразу же (как в случае синхронной операции), а через некоторое время, например, в виде некоторого асинхронного (нарушающего обычный порядок выполнения) вызова.

Долгое время программирование работы с удаленными ресурсами было достаточно проблематичным. Несмотря на неуклонный рост уровня абстракций для “локального” программирования, оставалась необходимость сделать операции с удаленными ресурсами более прозрачными, похожими на локальные, так чтобы разработчику не приходилось бороться с концептуальной избыточностью или, например, архитектурному “сопротивлению” несоответствиям между этими моделями.

Проблема заключается в том, что удаленные операции отличаются от локальных. Они различаются на порядки по задержкам доступа даже в лучшем случае, могут сбоить как-то по новому или просто никогда не вернуться, зависят от множества внешних факторов за пределами контроля или просто понимания разработчика.  И хотя они могут быть представлены как “просто вызовы методов”, это нежелательно, так как оставляет разработчика без возможности обработки особенных состояний, возникающих в силу их удаленности – отмена выполнения и обработка тайм-аутов, сохранение состояния потока во время блокирующего вызова, прогнозирование и обработка возможных проблем с сохранением восприимчивости к внешним действиям и т.п.

На текущий момент в .NET имелось несколько шаблонов, применяемых при асинхронном программировании, например, для работы с вводом/выводом и другими подобными операциями с большим временем доступы без блокирования потока. В большинстве случаев .NET предоставляет как синхронный (т.е. прозрачно блокирующий поток) и асинхронный (т.е. с явной задержкой выполнения) способы выполнения операций. Проблема заключается в том, что текущие шаблоны серьезно нарушают программную структуру, приводя к весьма сложному и подверженному ошибкам коду или (что гораздо чаще) разработчики сдаются и используют блокирующий подход, получая проблемы с сохранением восприимчивости к внешним событиям и производительностью взамен.

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


Нововведения в C#

С выходом Async CTP мы получили Community Technology Preview прототипа компилятора C# 5.0. Это именно прототип, предварительная версия для знакомства с новыми возможностями и в будущем все может измениться, даже синтаксис.

C# 5.0 вводит два новых ключевых слова await и async для реализации асинхронных функций в C#. В двух словах, модификатор async помечает метод или лямбда-выражение как асинхронные. Оператор await прекращает работу до тех пор, пока не будет завершена ожидаемая задача.

Другими словами, синхронный код:

загрузить данные, подождать результата, обработать, подождать окончания, сохранить результат, подождать…

преобразуется в асинхронный:

начать загрузку данных, когда будет закончено, начать обработку данных, когда будет закончено, начать сохранение результатов, когда результаты будут сохранены…

То есть, за счет введения двух новых ключевых слов типичный императивный код (сделай это, потом сделай то) преобразуется в декларативный (делай это, когда закончишь делай то). За декларативным асинхронным программированием стоит явное намерение эффективного использования доступных ресурсов. Anders Hejlsberg пообещал что wait+async это первые шаги для поддержки параллельного программирования на уровне языка.

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

Рекомендуемый паттерн в .NET для реализации асинхронных функций, возвращать задачу, представляющую выполняемую операцию и позволяющую ожидать ее окончания в какой-то момент времени. Асинхронные функции – это новая возможность в C#, предоставляющая упрощенные средства для выражения асинхронных операций.

Небольшой пример:

Task<Movie> GetMovieAsync(string title);
Task PlayMovieAsync(Movie movie);
async void GetAndPlayMoviesAsync(string[] titles)
{
    foreach (var title in titles)
    {
        var movie = await GetMovieAsync(title);
        await PlayMovieAsync(movie);
    }
}

По соглашению, асинхронные операции используют суффикс “Async”, чтобы показать, что частично их выполнение может быть произведено после возврата из метода. И GetMovieAsync, и PlayMovieAsync возвращают задачу, что обозначает, что их окончание в последствии можно ожидать. В противоположность этому GetAndPlayMoviesAsync возвращает void, так что ожидание его окончания (через await)  невозможно. Подобные асинхронные операции часто называют как “запустил и забыл” и могут быть полезны например для реализации асинхронных обработчиков событий.

GetAndPlayMoviesAsync помечен модификатором async как асинхронная функция, содержащая два await-выражения. Этот факт сообщает вызывающему о деталях реализации метода и фундаментально изменяет способ, которым метод выполняется: как только незавершенная задача (Task) начнет ожидание, управление будет возвращено вызывающему. Когда ожидаемая задача (Task) завершится, выполнение метода GetAndPlayMoviesAsync возобновится, до ожидания следующей незавершенной задачи и т.д. В промежутках между ожиданиями незавершенных задач, не занимается никакой дополнительный поток на выполнение этого метода, время “занимается” только в моменты активности.

Прежде чем двигаться дальше хочу прояснить частое заблуждение, связанное с понятием асинхронности.

Асинхронность не является синонимом “параллелизма” (concurrency) (одновременного (параллельного) выполнение компьютером нескольких операций), она может быть реализована с использованием этого подхода, но это не является обязательным. Другой пример реализации асинхронности – это разбиение начальной задачи на небольшие части, объединение их в очередь и затем выполнение обработки, когда поток не занят чем-то другим.

Итак, новые ключевые слова: await - указывает, что вызывающий хочет получить управление, когда вызов асинхронного метода будет завершен, async – это модификатор для метода или анонимной функции, показывающий, что они являются асинхронными. Функции без этого модификатора считаются синхронными.

Несколько замечаний:

  • Модификатор метода async не означает, что “данный метод будет автоматически запланирован к запуску в рабочем потоке асинхронно”. Он обозначает прямо противоположное, этот метод содержит управляющую логику, связанную с ожиданием асинхронных операций и тем самым будет перезаписан компилятором в стиле передачи продолжений (continuation passing style) для гарантии того, что асинхронные операции смогут продолжить выполнение этого места с корректной точки”. Главная особенность асинхронных методов состоит как раз в том, чтобы оставаться в текущем потоке как можно дольше. Они подобны сопрограммам (coroutines): асинхронные методы добавляют однопоточную кооперативную многозадачность в C#.
  • Оператор await не обозначает “эта операция блокирует текущий поток до возврата из асинхронной операции”. Это превратило бы асинхронные операции в синхронные, чего собственно и хотелось избежать. Смысл же противоположный: “если задача, которую мы ожидаем еще не завершена, то запишем остальную часть метода как продолжение этой задачи и сразу вернемся к вызывающему; задача продолжится (будет вызвано продолжение) по завершению асинхронной операции”.
  • Во время выполнения асинхронная функция может быть в одном из трех состояний: выполняется, приостановлена на await или выполнена.

“Путанница”

Исходный документ ((CSharp Spec) Asynchronous Functions) меня несколько ввел в заблуждение, возникло впечатление, что новый функционал  полностью завязан на Task. Однако, благодаря Reed Copsey, Jr., Jon Skeet и собственным экспериментам, удалось понять, что это не так.

Нововведения разделю на две части: изменения в языке и изменения в Framework.


Изменения в языке

Грамматика C# была расширена await-выражениями, используемыми для приостановки выполнения асинхронной функции, пока ожидаемая задача не будет выполнена. await-выражения допускаются только в теле асинхронных функций. Внутри асинхронной функции выражение не может использоваться в теле синхронной функции, в catch или finally блока оператора try, внутри lock или в небезопасном (unsafe) контексте. Однако, внутри самого try и, как следствие, в using вполне допустимы.

Шаблон await

Выражение t await-выражения “await t” называется задачей (это просто термин, не относящийся к типу Task) выражения. Задача t должна позволять ожидание (быть awaitable), что означает выполнение следующих условий:

  • (t).GetAwaiter() должно быть верным выражением типа A.
  • Для данного выражения a типа A и выражения r типа System.Action выражение (a).BeginAwait(r) должно быть верным логическим выражением.
  • Для заданного выражения a типа A, (a).EndAwait() должно быть верным выражением.

A называется ожидающим (awaiter) типом await-выражения. Метод GetAwaiter используется для получения awaiter-типа для задачи.

Метод BeginAwait используется для записи продолжения ожидающей задачи. Продолжение передаваемое в качестве параметра является делегатом возобновления.

Метод EndAwait используется для получения результата выполнения задачи, после ее завершения.

Вызовы методов разрешаются синтаксически, то есть GetAwaiter, BeginAwait и EndAwait могут быть как членами экземпляра, так и методами расширениями (extension) или даже привязанными динамически, главное чтобы вызов был верным в контексте await-выражения. Подразумевается, что все они должны быть “неблокирующими, то есть не приводят к значительному ожиданию вызывающего потока, например окончания завершения операции.

await-выражение классифицируется так же как и выражение (a).EndAwait(), то есть:

  • если выражение (a).EndAwait() вызывает метод или делгат, возвращающий тип void, то результатом и будет void.
  • в противном случае результат будет того типа, который возвращает (a).EndAwait().

Вычисление await-выражений

Во время выполнения тела асинхронной функции await t вычисляется следующим образом:

  • awaiter a получается путем вычисления выражения (t).GetAwaiter()
  • вычисляется логическое выражение (a).BeginAwait(r), где r – делегат, первое выполнение которого приведет к тому, что заключенная в выражении асинхронная функция будет продолжена с текущего await-выражения.
  • если результатом логического выражения является true вычисление приостанавливается и управление возвращается методу, который вызвал или возобновил выполнение асинхронной функции.
  • затем (или сразу, или во время возобновления) вычисляется выражение (a).EndAwait(). Если оно возвращает какое-то значение, то это значение и становится await-выражения.

Метод BeginAwait либо возвращает false, чтобы показать, что задача уже завершена, либо true, чтобы показать, что делегат r необходимо вызвать, когда задача в конце концов завершится. Возврат false позволяет выполнять ожидание (await) уже завершенных задач, предотвращая приостановку выполнения только для того, чтобы сразу оно было возобновлено.

Если BeginAwait возвращает true, то awaiter должен позаботиться о том, чтобы делегат r был вызван не больше раза.

Если BeginAwait возвращает false, то awaiter должен удостовериться, что делегат r не будет вызван ни разу. Если r не смотря на ограничения будет вызван, то поведение не определено.

Другими словами, если BeginAwait возращает true, то так или иначе должно быть вызвано продолжение, если нет, то это значит, что задача уже завершена и механизма продолжения не требуется. EndAwait просто выделяет, что считать результатом выполнения задачи.

На практике это означает, что на уровне компилятора нет никакой привязки к задачам (Task), все что надо, это реализация в классе или в расширениях трех методов: GetAwaiter, BeginAwait и EndAwait, и этого будет вполне достаточно для того, чтобы воспользоваться новым механизмом. Подробнее о разрешении методов можно почитать в статье Jon Skeet: C# 5 async: experimenting with member resolution (GetAwaiter, BeginAwait, EndAwait)

По существу, новые методы реализуют вполне привычную программную модель работы с асинхронными методами (Asynchronous Programming Model) с разделением задачи на Begin/End методы и методом обратного вызова для сигнализации окончания выполнения. Основное отличие в том, что теперь генерация подобных “оберток” делается компилятором автоматически.

В подавляющем большинстве случаев, для практического применения “ручного” задания класса, с соответствующим интерфейсом не потребуется, так как для большинства задач, в которых применение нового асинхронного шаблона имеет смысл (например, задачи ввода/вывода, сложные расчетные задачи), удобно реализовывать с помощью нового для .NET 4 Task из Task Parallel Library, а await-выражения с Task поддерживаются непосредственно, без необходимости писать дополнительный код. Так же в Framework добавлено множество расширений стандартных классов возвращающих Task<TResult> в качестве результата, что позволяет так же использовать эти методы непосредственно в await.

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

class Awaiter
{
    public Awaiter GetAwaiter()
    {
        return this;
    }
 
    public bool BeginAwait(Action continuation)
    {
        return true;
    }
    
    public void EndAwait()
    {
    }
}

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

На всякий случай замечу, никто не заставляет повторять вручную и писать собственные реализации с Task для работы с асинхронными функциями вместо тех, что предоставляет Framework. Но понимать, происходящие внутри процессы весьма важно. Именно, поэтому я на них специально остановился. За более подробными примерами можно обратиться к статье Jon Skeet: C# 5 async: investigating control flow


Что делает компилятор?

На самом деле, компилятор выполняет обработку сильно напоминающую обработку для итераторов (yield return).

Асинхронный метод – это не обычный последовательный метод, он преобразуется в конечный автомат (объект) с некоторым набором состояний (локальные переменные становятся полями этого объекта), каждый блок кода между последовательными использованиями await является одним “шагом” автомата.

Это обозначает, что когда метод запускается на выполнение, он просто делает первый шаг и затем конечный автомат возвращается, запланировав какие-то действия, когда эти действия будут выполнены, запустится следующий шаг автомата.

Для примера, следующий код:

async Task TestAsync() 
{ 
    var var1 = F(); 
    var var2 = await FAsync(); 
    more(var1, var2);
}
 
Будет преобразован в нечто подобное:
 
class _TestAsync 
{
  int _var1, _var2;
  int _state = 0; 
 
  Task<int> _await1;
 
  public void DoStep() 
  {
    switch(_state)
    {
        case 0: 
          _var1 = F();
          _await1 = FAsync();
      
          _state = 1;

SetContinuation(DoStep); // По завершению асинхронной операции необходимо вызвать этот же метод

 
          case 1:

// Получение результата операции

_var2 = _await1.Result;

 

   more(_var1, _var2);
  }
}
Важным здесь является использование метода SetContinuation(), для указания что когда операция будет завершена, необходимо вызвать еще раз метод DoStep(), а за счет того, что состояние изменилось (переменная _state), при повторном вызове будет выполнена вторая часть метода (case 1).

Данное преобразование очень близко к аналогичному для итераторов и на деле, одним из приемов для упрощения работы с асинхронными методами и является их использование. Для примера, можно вспомнить библиотеку PowerThreading от Jeffrey Richter или статью Asynchronous Programming in C# using Iterators, где рассматриваются схожие идеи.

На самом деле, преобразование несколько сложнее так как помимо продолжения операции необходимо, например, учитывать возможные исключения в процессе работы.  Примеры преобразований приведены в конце новой спецификации ((CSharp Spec) Asynchronous Functions (документация Async CTP)), посмотрите на раздел Implementation example, колонку Syntactic expansion. Кроме этого, можно просто скомпилировать какой-нибудь пример и глянуть результат Reflector’ом.


Изменения в Framework

Для упрощения перехода на новую модель команда разработки Async CTP расширила с использованием нового подхода класс Task, представив новый шаблон Task-based Asynchronous Pattern (TAP) (Асинхронный шаблон проектирования на основе задач), подробности можно прочитать в документах Asynchrony in .NET и The Task-based Asynchronous Pattern (находятся в каталоге документации после установки Async CTP). На данный момент они находятся в сборке AsyncCtpLibrary.dll.

Еще раз обращаю внимание: расширение класса Task и асинхронные решения на его основе, являются дополнением Framework, а не компилятора и призваны упростить миграцию кода в соответствии с новым подходом. Для компилятора нет разницы используется ли класс Task или другой класс, при условии следования описанным правилам. Но реальная реализация большинства новых методов, выполнена за счет расширения класса Task  методом GetAwaiter(), возвращающим структуру TaskAwaiter и для дальнейшей работы используются уже её методы BeginAwait и EndAwait.

Почему за основу взят именно класс Task? 

Причина заключается в том, асинхронность не требует параллелизма, но параллелизм требует асинхронности и многие инструментальные средства полезные для параллелизма могут быть так же легко использованы для непараллельной асинхронности. В Task нет неотъемлемого присутствия параллелизма, Task Parallel Library (TPL) использует шаблон на основе задач для представления частей выполняющейся задачи, которые могут быть распараллелены, не требуя многозадачности.

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

В класс Task из TPL вложено очень многое: у него есть механизм отмены (cancellation) и другие полезные возможности. Вместо изобретения чего-то нового наподобие “IFuture”, просто был расширен существующий код для удовлетворения асинхронным потребностям.

AsyncCtpLibrary включает в себя множество extension-методов для существующих в Framework классов, с тем, чтобы предоставить асинхронные варианты существующих методов. По соглашению, эти методы названы с суффиксом Async, например CopyToAsync для асинхронного копирования потока. В случае, если в классе был уже метод с суффиксом Async, то для избежания путаницы  в таких классах методы имеют суффикс TaskAsync, например, DownloadDataTaskAsync. Все эти методы приведены в соответствие требованиям для возможности использования вместе с await.

Сам класс Task так же был расширен (структура TaskAwaiter), чтобы позволять своё использование с await. Кроме того, “пригодились” и изначально заложенные в TPL возможности по взаимодействию с существующей асинхронной моделью, семейство методов TaskFactory.FromAsync, использующихся для конструирования Task из пары Begin/End методов “старой” модели асинхронного программирования (Asynchronous Programming Model).

Помимо расширений для классов Framework для возможности использования с await, добавились и дополнительные методы, например, Yield, используется, чтобы временно прервать выполнение, позволяя продолжиться другим операциям, SwitchTo используется для переключения контекста (SynchronizationContext) выполнения операции. Например,

public async void button1_Click(object sender, EventArgs e)
{
    string text = txtInput.Text;
 
    await ThreadPool.SwitchTo(); // переключение в ThreadPool
 
    string result = ComputeOutput(text);
    string finalResult = ProcessOutput(result);
 
    await txtOutput.Dispatcher.SwitchTo(); // переключение в поток TextBox
 
    txtOutput.Text = finalResult;    
}

Нововведений довольно много, в короткой заметке все не охватить за примерами, приемами работы и самое главное подробностями рекомендую обратиться к The Task-based Asynchronous Pattern от Stephen Toub, где они весьма неплохо рассмотрены.


Как это работает на уровне Task (Task-based Asynchronous Pattern)?

Async CTP помимо нововведений в язык, ориентирована на использование нового механизма непосредственно с TPL, поддерживая await-выражения непосредственно над Task.

На уровне API неблокирующее ожидание достигается за счет предоставления методов обратного вызова (callbacks), вызываемых после завершения операции. Для задач (Task) это же достигается для счет методов типа ContinueWith. Поддержка асинхронного подхода в языке прячет обратные вызовы, позволяя асинхронным операциям ожидать окончания выполнения в обычном порядке за счет генерируемого компилятором кода.

Как я уже писал, await непосредственно поддерживает асинхронное ожидание Task и Task<TResult>. На данный момент это достигается за счет соответствующих методов расширения Task (см. структуру TaskAwaiter), а не компилятором непосредственно. Для Task результат await-выражения - void, для Task<TResult> – результат TResult.

“Внутри”, для поддержки функционала await устанавливается метод обратного вызова на задачу через продолжение. Этот метод продолжит выполнение асинхронного метода с точки остановки. Когда выполнение асинхронного метода будет возобновлено, если задача Task<TResult> находилась в ожидании и завершилась успешно, то будет возвращен TResult.

Если выполнение Task или Task<TResult> завершилось состоянием отмены (Canceled state), то будет выброшено исключение OperationCanceledException.

Если выполнение Task или Task<TResult> завершилось состоянием сбоя (Faulted state), то будет выброшено соответствующее исключение, вызвавшее его. Возможно, что сбой будет вызван множеством исключений, в этом случае только одно из них будет выброшено, однако свойство Exception в Task будет содержать AggregateException, содержащий все ошибки.

Если с потоком, выполняющим асинхронный метод, был ассоциирован SynchronizationContext  на момент приостановки (другими словами SynchronizationContext.Current  не null), возобновление работы асинхронного метода будет произведено в том же SynchronizationContext за счет метода Post. В противном случае, возобновление будет зависеть от текущего System.Threading.Tasks.TaskScheduler на момент приостановки (как правило это TaskScheduler.Default, соответствующий .NET ThreadPool). Окончательное решение: разрешить возобновление непосредственно по окончанию выполнения асинхронного метода, или запланировать возобновление выполнения на будущее за TaskScheduler .

В момент вызова асинхронный метод выполняет тело метода синхронно до первого await-выражения, в этой точке он возвращает управление вызывающему. Если асинхронный метод не возвращает void, то результатом будет Task или Task<TResult>, представляющие продолжающиеся вычисления (напомню, в данный момент речь о поведении Framework-методов, а не компилятора в целом).

После выполнения первого await-выражения, оставшаяся часть тела метода эффективно выполняется одновременно (параллельно) с кодом, вызвавшем асинхронный метод. Когда же ожидаемая операция в теле метода завершится, начинается выполнение следующей части метода. Если встречается оператор return или будет достигнут конец асинхронного метода, задача (Task) считается завершенной в состоянии RanToCompletion. Если будет выброшено необработанное исключение, прерывающее выполнение метода (выход за тело асинхронной функции), задача завершается состоянием Faulted (если это исключение OperationCanceledException, то вместо этого задача будет в состоянии Canceled). В любом из вариантов, будет либо возвращен результат операции, либо исключения помешавшие этому.

Существует несколько важных отличий от описанного поведения. Для повышения производительности, в случае если задача уже завершила работу к моменту начала ожидания, переключения управления не будет производиться, а вместо этого сразу же продолжится выполнение метода. Кроме того, не всегда удобно и необходимо переключаться в тот же контекст из которого было начато ожидание. Подробнее об этом можно почитать у Stephen Toub в The Task-based Asynchronous Pattern (документация Async CTP).


TPL Dataflow

Еще одно интересное добавление. TPL Dataflow (TDF) построена поверх функционального уровня TPL и дополняет его набором примитивов, для решения более сложных задач чем исходная библиотека. TDF использует задачи, потоково-безопасные коллекции и другие возможности, представленные в .NET 4 для добавления поддержки параллельной обработки параллельного потока данных. Она так же непосредственно интегрируется с новой поддержкой в языке асинхронного программирования.

Многие программные системы ориентированы на обработку потоков данных. Эти потоки данных часто большие или даже бесконечные, и/или требуют сложной обработки, приводя к потребностям в высокой скорости обработки и потенциально к очень большой вычислительной нагрузке. Чтобы справится с этими требованиями, были введены средства для распараллеливания обработки на доступные системные ресурсы, например, несколько ядер. Однако, модель параллельного программирования в сегодняшнем .NET Framework’е (включая .NET 4) не была разработана с мыслью о потоках данных, приводя к усложнению кода, низкоуровневым блокировкам и т.п., в  итоге уменьшая производительность системы, вводя проблемы с параллелизмом, которых можно было бы избежать в случае выбора модели более подходящей реактивным системам.

TDF привносит несколько ключевых примитивов в TPL, позволяющих разработчикам описывать обработку основанную на графах потоков данных. Данные всегда обрабатываются асинхронно. С этой моделью нет необходимости явно запускать задачи для обработки, разработчики декларативно описывают зависимости между данными и среда выполнения планирует обработку основанную на асинхронном поступлении данных и описанных зависимостях. Таким образом, TDF генератор для задач (Task) подобно классу Parallel в PLINQ, но Parallel  и PLINQ специализируются на более структурированной модели параллельных вычислений, а TDF специализируется на неструктурированной асинхронной модели.

Есть некоторое подобие между TDF и Rx. Но если Rx преимущественно специализируется на координировании и композиции потоков событий с API основанном на LINQ, предоставляя богатый набор комбинаторов для манипулирования данными IObservable<T>. В противоположность этому, TDF специализируется на строительных блоках для передачи сообщений и распараллеливания в процессорно- и IO-нагруженных приложениях, предоставляя при этом разработчикам явный контроль надо тем как данные буферизируются и перемещаются внутри системы.

Пример:

Класс ActionBlock<TInput> можно воспринимать логически как буфер данных, которые необходимо обработать вместе с задачами по обработке. В большинстве случаев можно просто создать экземпляр класса и “отправить” ему данные, делегат указанный при создании ActionBlock, будет асинхронно выполняться для каждого отправленного набора данных.

var ab = new ActionBlock<TInput>(delegate(TInput i)
{
    Compute(i);
});
ab.Post(1);
ab.Post(2);
ab.Post(3);

Второй пример:

Другим примером может быть тип BufferBlock<T>, который можно считать как несвязанный буфер для данных, разрешающий как синхронные так и асинхронные сценарии производитель/потребитель. Для примера ниже код предающий данные между множеством производителей и потребителей.

private static BufferBlock<int> m_buffer = new BufferBlock<int>();
 
// Производитель
private static void Producer()
{
    while(true)
    {
        int item = Produce();
        m_buffer.Post(item);
    }
}
 
// Потребитель
private static async Task Consumer()
{
    while(true)
    {
        int item = await m_buffer.ReceiveAsync();
        Process(item);
    }
}

TDF очень интересна, рекомендую почитать более подробно о ней в документации.


Jeffrey Richter’s AsyncEnumurator и Async CTP

В своей рассылке, посвященной PowerThreading, Jeffrey Richter прокомментировал новый Async CTP. Переведу от его лица.

Я был серьёзно вовлечен в создание новых асинхронных возможностей, которые Microsoft добавила в следующие версии C#/VB.

Мы проводили ежемесячные встречи и опрос, который я просил пройти членов этой группы несколько месяцев назад, непосредственно повлиял на итоговую реализацию.

По существу, новая возможность языка очень похожа на итераторы C# с тем исключением, что вместо ‘yield return’ используется await. Новое ключевое слово более естественно и может использоваться для выражения контекста, в противоположность операторному содержимому (yield return), который просто упрощает ваш код, улучшая его компоновку. Кроме того, асинхронные методы могут вызывать другие асинхронные методы, что является огромным улучшением  по сравнению с тем, что вам позволяли делать итераторы. Это позволяет вам иметь асинхронные подпрограммы, которые можно вызывать из других асинхронных (и не асинхронных) методов для улучшения структуры. Мой AsyncEnumerator так же поддерживает это, но довольно топорно.

Новые асинхронные возможности сконцентрированы на Task, что предоставляет три больших преимущества. Во-первых, это намного проще использования Begin/End/IAsyncResult. Во-вторых, это объединяет выполнение вычислительных операций и операций ввода/вывода вокруг единственной конструкции. В-третьих, они интегрируют работу с SynchronizationContext, что само по себе является большим улучшением по сравнению с Begin/End. Все это снижает сложность понимания концепции программистами и делает принципы работы более естественными. Кроме того, я не знаю собирается ли MS официально объявлять устаревшей или неподдерживаемой асинхронную модель на основе событий (event-based APM) (наподобие классов BackgroundWork и WebClient), но они непременно должны воспрепятствовать использованию этих классов людьми, использующими шаблоны на основе событий. Этот шаблон всегда имел множество проблем и я твёрдо уверен, что он вообще не должен использоваться. Новые асинхронные возможности намного лучше всего, что может предложить событийный подход.

Что касается AsyncEnumerator:

  • Он работает на .NET 2.0 и выше. Новые асинхронные возможности не будут работать на этих платформах, по факту они еще даже не выпущены.
  • Мне необходимо подробнее изучить детали, но я практически уверен, что новые асинхронные возможности имеют немного большие накладные расходы (overhead) по сравнению с AsyncEnumerator. С новыми возможностями вы создаете Task (объект с большим количеством полей в нем), оборачивая существующие в Framework Begin/End. То есть заканчиваете тем, что выделяете больше памяти и выполняете вызов вызова. Для людей, которые действительно беспокоятся о памяти/производительности, я практически уверен, что мой AsyncEnumerator использует меньше памяти и выполняется быстрее. Я не делал еще никаких исследований, чтобы привести конкретные числа.
  • Мой AsyncEnumerator по-прежнему предлагает некоторые возможности, которых нет в асинхронных нововведениях. Например, поддержка отладки в AsyncEnumerator гораздо лучше. AE так же дает вам возможность перехватывать события приостановки/возобновления в конечном автомате, это потребовалось для некоторых из моих клиентов для переноса состояния между потоками в процессе переключения конечного автомата с потока на поток. Я так же предоставляю способ не использовать SynchronizationContext, так что часть работы может быть выполнена в потоке GUI, а часть в рабочем потоке (прим. такая возможность есть и у await/async). Новый асинхронный функционал всегда использует SynchronizationContext. Наконец, в некоторых случаях кодирование с использованием AE немного проще. Для примера, с использованием AE вы можете легко ждать окончания ‘n’ операций, в нововведении вам придется использовать методы-комбинаторы.

Мне нравятся нововведения и это абсолютно точно правильное направление движения. Помните, что прямо сейчас они находятся в зародыше и в будущем команда добавит более хорошую поддержку отладки и другие возможности. Мой AsyncEnumerator будет оставаться доступным для всех кому потребуется поддержка .NET 2.0 и старше и я продолжу адаптировать его к новым возможностям C#/.NET, так чтобы людям не пришлось портировать весь свой код на новую модель. Я попробую улучшить новые асинхронные возможности лучшей поддержкой отладки и всем остальным, что предлагает AE на сегодняшний день. Я чувствую себя крайне ответственным перед пользователями AsyncEnumerator и собираюсь продолжать работу над ним. К тому же, я безусловно быстрее чем Microsoft смогу ответить на запрос и добавить новые возможности или исправления ошибок.

Лично, я очень горжусь AsyncEnumerator  и его успехом, сыгравшем огромную роль в индустрии и тем, что я смог помочь Microsoft в разработке новых асинхронных возможностей. Моей целью всегда было попытаться упростить их использование для других для построения масштабируемых и быстро реагирующих приложений и компонентов и чувство, что я играю значительную роль в этом очень приятно.

 


Async CTP и R#

В ReSharper поддержки для нового Async CTP пока не будет. Безусловно, поддержка для нового C# будет разрабатываться, когда он хоть как-то будет стандартизирован, в данный момент об этом говорить еще рано, так как все может еще поменяться, даже синтаксис.

Так что, пока придется не обращать внимания на ошибки при открытии проектов, использующих новый синтаксис.

Проблема, не том, что придется поправить синтаксический анализатор, и даже не столько в том, что придется повторить работу компилятора, для соответствующей трансформации исходного кода. R# работает со “сломанным” исходным кодом, выполняя его анализ и обработку, придется внести существенные изменения в эти алгоритмы, а поскольку все еще может измениться, то это было бы довольно непрактичным.


Информация по теме (или с чего начать?)

Visual Studio Async CTP (Страница загрузки)

The Future of C# and Visual Basic (PDC10 session by Anders Hejlsberg)

Asynchronous Programming for C# and Visual Basic (MSDN)

Visual Studio Async CTP Forum

C# Language Specification for Asynchronous Functions - (CSharp Spec) Asynchronous Functions (документация Async CTP)

Asynchrony in .NET 

Stephen Toub, The Task-based Asynchronous Pattern (документация Async CTP)

Установка Async CTP и начало работы, так же советую после установки заглянуть в документ Walkthrough: Getting Started with Async (документация Async CTP)

Блог Эрика Липперта по теме асинхронного программирования

Дмитрий Нестерук - Async CTP: Async, Await и C#5

Reed Copsey: C# 5 Async, Part 1: Simplifying Asynchrony – That for which we await

Jon Skeet: Initial thoughts on C# 5's async support

Anoop Madhusudanan: C# 5.0 Asynchrony – A Simple Intro and A Quick Look at async/await Concepts in the Async CTP

TPL With Other Asynchronous Patterns – использование TPL с другими асинхронными шаблонами.

Introduction to TPL Dataflow (документация Async CTP)

Видео от ключевых разработчиков: Mads Torgersen, Lucian Wischik и Stephen Toub

Интереснейшая серия статей по сравнению концепций асинхронного программирования в F# и C# от Tomas Petricek- Asynchronous C# and F# (I.): Simultaneous introduction


Обещанный пример консольного приложения, реализующего шаблон await-выражений, поддерживаемый новым компилятором. Ценность этого примера весьма сомнительна, но тем не менее, он позволит вам изучить, например, с помощью Reflector как именно производится трансформация исходного кода. Не забудьте добавить в References AsyncCtpLibrary.dll, чтобы не обижать компилятор.

 

using System;
 
namespace AsyncTesters
{
    class Program
    {
        class Awaiter
        {
            private Action _continuation;
 
            public Awaiter GetAwaiter()
            {
                Console.WriteLine("Init awaiter");
                return this;
            }
 
            public bool BeginAwait(Action continuation)
            {
                Console.WriteLine("Begin await: {0}", continuation.Method.Name);
                _continuation = continuation;
                return true;
            }
            
            public void EndAwait()
            {
                Console.WriteLine("End await");
            }
 
            public void Continue()
            {
                _continuation();
            }
        }
 
        async static void Test(Awaiter s)
        {
            Console.WriteLine("Start testing");
            await s;
 
            Console.WriteLine("End testing");
        }
 
        static void Main(string[] args)
        {
            var awaiter = new Awaiter();
            Test(awaiter);
 
            Console.WriteLine("Test returns");
 
            awaiter.Continue();
 
            Console.ReadLine();
        }
    }
}

После запуска на консоль должно вывестись:

Start testing
Init awaiter
Begin await: MoveNext
Test returns
End await
End testing

Повторюсь, особой практической ценности пример не несет, разве что демонстрирует идею использования нового механизма в компиляторе. За более подробными примерами можно обратиться к статье Jon Skeet: C# 5 async: investigating control flow

Удачи!