Как C# async/await относится к более общим конструкциям, например, рабочим процессам F# или монадам?

дизайн языка C# всегда (исторически) был ориентирован на решение конкретных проблем, а не на поиск решения основных общих проблем: см., например,http://blogs.msdn.com/b/ericlippert/archive/2009/07/09/iterator-blocks-part-one.aspx для "IEnumerable vs. coroutines":

мы могли бы сделать его гораздо более общим. Наш итератор блоков можно рассматривать как слабый вид сопрограмма. Мы могли бы выбрать, чтобы реализовать полный сопрограммы и просто сделал итератор блоков частным случаем сопрограммы. И конечно, сопрограммы, в свою очередь, меньше, чем первого класса продолжений; мы могли ввести продолжений, реализуемых сопрограммы в плане продолжений и итераторы в плане сопрограммы.

или http://blogs.msdn.com/b/wesdyer/archive/2008/01/11/the-marvels-of-monads.aspx для метода SelectMany в качестве суррогата (своего рода) монады:

C# система типов недостаточно мощна, чтобы создать обобщенную абстракцию для монад, которая была основным мотиватором для создания методов расширения и"шаблона запроса"

Я не хочу спрашивать, почему это было так (многие хорошие ответы уже были даны, особенно в блоге Эрика, которые могут применяться ко всем этим проектным решениям: от производительности до повышенной сложности, как для компилятора, так и для программиста).

то, что я пытаюсь понять, к которому "общая конструкция" ключевые слова async/await относятся к (Мое лучшее предположение-монада продолжения-в конце концов, F# async реализуется с использованием рабочих процессов, которые, по моему пониманию, являются монадой продолжения), и как они относятся к ней (как они отличаются? чего не хватает?, почему существует разрыв, если он есть?)

Я ищу ответ, похожий на статью Эрика Липперта, которую я связал, но связанный с async/await вместо IEnumerable/yield.

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

2 ответов


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

объяснение этого выходит далеко за рамки одного ответа SO, но позвольте мне объяснить ключ помыслы.

монадических операций. Асинхронный код C# по существу состоит из двух примитивных операций. Ты можешь!--6--> асинхронное вычисление, и вы можете return результат асинхронного вычисления (в первом случае это делается с использованием нового ключевого слова, а во втором случае мы повторно используем ключевое слово, которое уже находится на языке).

если вы следовали общей схеме (монады), то вы бы переводить асинхронный код в вызовы следующих двух операций:

Task<R> Bind<T, R>(Task<T> computation, Func<T, Task<R>> continuation);
Task<T> Return<T>(T value);

они оба могут быть довольно легко реализованы с помощью стандартного API задач - первый из них по существу представляет собой комбинацию ContinueWith и Unwrap а второй просто создает задачу, которая немедленно возвращает значение. Я собираюсь использовать вышеупомянутые две операции, потому что они лучше захватывают идею.

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

давайте рассмотрим случай, когда мы avait выражение e а затем присвоить результат переменной x и оценить выражение (или блок оператора)body (В C# вы можете ждать внутри выражения, но вы всегда можете перевести это в код, который сначала назначает результат переменной):

[| var x = await e; body |] 
   = Bind(e, x => [| body |])

я использую нотацию, которая довольно распространена в языках программирования. Смысл [| e |] = (...) это то, что мы переводим выражение e (в" семантических скобках") к некоторому другому выражению (...).

в приведенном выше случае, когда у вас есть выражение с await e, переводится на Bind операция и тело (остальная часть кода после await) помещаются в лямбда-функцию, которая передается в качестве второго параметра в Bind.

вот где происходит интересная вещь! Вместо оценки остальной части кода тут (или блокировка потока во время ожидания),Bind операция может запускать асинхронную операцию (представленную e типа Task<T>) и, когда операция завершается, она может, наконец, вызвать лямбда-функцию (продолжение) для запуска остальной части тела.

идея перевода заключается в том, что он превращает обычный код, который возвращает некоторый тип R к задаче, которая возвращает значение асинхронно-то есть Task<R>. В выше уравнение, возвращаемый тип Bind - это, действительно, задача. Вот почему нам нужно перевести return:

[| return e |]
   = Return(e)

это довольно просто-когда у вас есть результирующее значение, и вы хотите его вернуть, вы просто обертываете его в задачу, которая немедленно завершается. Это может показаться бесполезным, но помните, что нам нужно вернуть Task потому что Bind операция (и весь наш перевод) требует.

большего примера. если вы посмотрите на больший пример, содержащий несколько awaits:

var x = await AsyncOperation();
return await x.AnotherAsyncOperation();

код будет переведен примерно так:

Bind(AsyncOperation(), x =>
  Bind(x.AnotherAsyncOperation(), temp =>
    Return(temp));

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

продолжение монады. в C# асинхронный механизм фактически не реализован с использованием вышеуказанного перевода. Причина в том, что если сосредоточиться просто на async вы можете сделать более эффективную компиляцию (что и делает C#) и создать государственную машину напрямую. Тем не менее, выше в значительной степени, как асинхронные рабочие процессы работают в F#. Это также источник дополнительной гибкости в F# - вы можете определить свой собственный Bind и Return для обозначения других вещей - таких как операции по работе с последовательностями, ведение журнала отслеживания, создание возобновляемых вычислений или даже объединение асинхронных вычислений с последовательностями (асинхронная последовательность может дают несколько результатов, но также могут ждать).

реализация F# основана на продолжение монады что означает Task<T> (на самом деле,Async<T>) в F# определяется примерно так:

Async<T> = Action<Action<T>> 

то есть, асинхронное вычисление определенных действий. Когда вы даете его Action<T> (продолжение) в качестве аргумента он начнет выполнять некоторую работу, а затем, когда он в конечном итоге завершится, он вызовет это действие, которое вы указали. Если вы ищете монады продолжения, тогда я уверен, что вы можете найти лучшее объяснение этому как в C#, так и в F#, поэтому я остановлюсь здесь...


ответ Томаса очень хорош. Чтобы добавить еще несколько вещей:

дизайн языка C# всегда (исторически) был ориентирован на решение конкретных проблем, а не на поиск решения основных общих проблем

хотя есть некоторые

это конечно, верно, что есть спектр С "очень конкретным "на одном конце и" очень общим " на другом, и что решения конкретных проблем попадают на этот спектр. C# разработан в целом, чтобы быть очень общим решением многих конкретных проблем; это то, что является языком программирования общего назначения. Вы можете использовать C# для записи всего, от веб-служб до игр XBOX 360.

поскольку C# предназначен для языка программирования общего назначения, когда проектная группа определяет конкретную проблему пользователя, которую они всегда рассматривают в более общем случае. LINQ является отличным примером. В самые первые дни разработки LINQ это было немного больше, чем способ поместить операторы SQL в программу C#, потому что это проблемное пространство, которое было определено. Но довольно скоро в процессе проектирования команда поняла, что понятия сортировки, фильтрации, группировки и объединения данных применимы не только к табличным данным в реляционной базе данных, но и иерархические данные в XML и для специальных объектов в памяти. И поэтому они решили пойти на гораздо более общее решение, которое мы имеем сегодня.

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

действительно основные функции, добавленные с C# 1.0 -- общие типы, анонимные функции, итератор блоки, LINQ, dynamic, async -- все имеют свойство, что они являются очень общими функциями, полезными во многих разных доменах. Все они могут рассматриваться как конкретные примеры более общей проблемы, но это верно для любой решение любой проблема, вы всегда можете сделать его более общим. Идея дизайна каждой из этих функций найти точки, где они не могут быть сделаны более общие не путая своих пользователей.

теперь что я отрицал предпосылку вашего вопроса, давайте посмотрим на фактический вопрос:

Я пытаюсь понять, к какой "общей конструкции" относятся ключевые слова async/await

Это зависит от того, как вы на это смотрите.

функция async-await построена вокруг Task<T> введите, как вы заметили, монаду. И, конечно, если бы вы поговорили об этом с Эриком Мейером, он бы сразу указал на это!--0--> на самом деле comonad; вы можете получить T значение обратно на другой конец.

другой способ взглянуть на эту функцию-взять абзац, который вы процитировали о блоках итератора, и заменить "итератор" на "асинхронный". Асинхронные методы, как методы итераторов, вид сопрограмма. Вы можете думать о Task<T> Как просто деталь реализации механизма корутина, если хотите.

третий способ взглянуть на функции стоит сказать, что это своего рода call-with-current-continuation (обычно сокращенно call/cc). Это не полная реализация call / cc, потому что она не принимает состояние стека вызовов во время регистрации продолжения. См. этот вопрос для деталей:

как новая асинхронная функция в c# 5.0 может быть реализована с помощью call / cc?

Я буду ждать и посмотреть, если кто-то (Эрик? Джон? может, ты?) может заполнить более подробную информацию о том, как на самом деле генерирует C# код для реализации ожидайте,

переписывание-это, по сути, просто вариант того, как переписываются блоки итераторов. Мэдс проходит через все детали в своей статье журнала MSDN:

http://msdn.microsoft.com/en-us/magazine/hh456403.aspx