Как реализовать новую функцию асинхронности в C# 5.0 с помощью call/cc?
Я следил за новым объявлением о новом async
функция, которая будет в C# 5.0. У меня есть базовое понимание стиля передачи продолжения и преобразования, которое новый компилятор C# делает в коде, подобном этому фрагменту из поста Эрика Липперта:
async void ArchiveDocuments(List<Url> urls)
{
Task archive = null;
for(int i = 0; i < urls.Count; ++i)
{
var document = await FetchAsync(urls[i]);
if (archive != null)
await archive;
archive = ArchiveAsync(document);
}
}
Я знаю, что некоторые языки реализуют продолжения с помощью вызова с текущим продолжением (callcc
), но я не очень понимаю, как это работает или что именно.
Итак, вот вопрос: если Андерс и соавт. решил прикусить пулю и просто реализовать callcc
в C# 5.0 вместо async
/await
особый случай, как будет выглядеть приведенный выше фрагмент?
3 ответа
Оригинальный ответ:
Насколько я понимаю, ваш вопрос таков: "Что если вместо реализации" ожидают "специально для основанной на задачах асинхронности, скорее, была реализована более общая операция потока управления вызовом с текущим продолжением?"
Ну, во-первых, давайте подумаем о том, что делает "ожидание". "ожидание" принимает выражение типа Task<T>
, получает ожидающего и вызывает ожидающего с текущим продолжением:
await FooAsync()
становится эффективно
var task = FooAsync();
var awaiter = task.GetAwaiter();
awaiter.BeginAwait(somehow get the current continuation);
Теперь предположим, что у нас был оператор callcc
который принимает в качестве аргумента метод и вызывает метод с текущим продолжением. Это будет выглядеть так:
var task = FooAsync();
var awaiter = task.GetAwaiter();
callcc awaiter.BeginAwait;
Другими словами:
await FooAsync()
не более чем
callcc FooAsync().GetAwaiter().BeginAwait;
Это отвечает на ваш вопрос?
Обновление № 1:
Как указывает комментатор, в приведенном ниже ответе предполагается использование шаблона генерации кода из версии "Предварительный просмотр технологии" функции async/await. Мы на самом деле генерируем немного другой код в бета-версии функции, хотя логически это то же самое. Настоящий кодоген похож на:
var task = FooAsync();
var awaiter = task.GetAwaiter();
if (!awaiter.IsCompleted)
{
awaiter.OnCompleted(somehow get the current continuation);
// control now returns to the caller; when the task is complete control resumes...
}
// ... here:
result = awaiter.GetResult();
// And now the task builder for the current method is updated with the result.
Обратите внимание, что это несколько сложнее, и обрабатывает случай, когда вы "ожидаете" результат, который уже был вычислен. Нет необходимости проходить через весь ригамарол, связанный с передачей контроля вызывающей стороне и повторным поиском с того места, где вы остановились, если результат, которого вы ожидаете, фактически уже кешируется в памяти для вас прямо здесь.
Таким образом, связь между "await" и "callcc" не так проста, как это было в предварительном выпуске, но все же ясно, что мы по сути делаем callcc для метода "OnCompleted" ожидающего. Мы просто не делаем callcc, если нам не нужно.
Обновление № 2:
Как этот ответ
/questions/8180607/c-zhdu-prodolzheniya-ne-sovsem-to-zhe-samoe/8180617#8180617
по словам Тимви, семантика call/cc и await не совсем одинакова; "истинный" вызов / cc требует либо "захвата" всего продолжения метода, включая весь стек вызовов, либо, что то же самое, переписывания всей программы в стиль передачи продолжения.
Функция "ожидание" больше похожа на "совместный вызов /cc"; продолжение содержит только то, "что текущий метод возврата задачи собирается сделать следующим в точке ожидания?". Если вызывающий метод, возвращающий задачу, собирается сделать что-то интересное после того, как задача завершена, он может зарегистрировать его продолжение как продолжение задачи.
Я не эксперт в продолжениях, но попробую объяснить разницу между async/await и call/cc. Конечно, это объяснение предполагает, что я понимаю call / cc и async/await, что я не уверен, что понимаю. Тем не менее, здесь идет...
В C# 'async' вы говорите компилятору генерировать специальную версию этого конкретного метода, который понимает, как поместить его состояние в структуру данных кучи, чтобы его можно было "удалить из реального стека" и возобновить позже. Внутри асинхронного контекста "await" тогда похож на "call / cc" в том смысле, что он использует объекты, сгенерированные компилятором, для ограничения состояния и выхода из "реального стека" до завершения задачи. Однако, поскольку переписывание асинхронного метода компилятором позволяет ограничивать состояние, await можно использовать только в контексте асинхронности.
В первоклассном вызове / cc языковая среда выполнения генерирует весь код, так что текущее продолжение может быть заключено в функцию продолжения вызова (что делает ненужным ключевое слово async). call / cc по-прежнему действует как ожидание, в результате чего текущее состояние продолжения (представьте состояние стека) будет загружено и передано как функция вызываемой функции. Один из способов сделать это - использовать кадры кучи для всех вызовов функций вместо стековых фреймов. (иногда называемый "стек без стека", как в "Python без стека" или во многих реализациях схемы). Другой способ - удалить все данные из "реального стека" и поместить их в структуру данных кучи перед вызовом цели call/cc.,
Некоторые хитрые проблемы могут возникнуть, если есть вызовы внешних функций (например, DllImport), смешанные в стеке. Я подозреваю, что это причина, по которой они пошли с реализацией async/await.
http://www.madore.org/~david/computers/callcc.html
Поскольку в C# функция должна быть помечена как "асинхронная", чтобы использовать эту механику, мне интересно, станет ли это ключевое слово async вирусом, распространяющимся по множеству функций во многих библиотеках. Если это произойдет. в конечном итоге они могут понять, что должны реализовать первоклассный вызов / cc на уровне виртуальной машины вместо этой асинхронной модели, основанной на перезаписи компилятора. Время покажет. Тем не менее, это, безусловно, полезный инструмент в контексте текущей среды C#.
Вроде бессмысленно скажу. Интерпретаторы схемы часто реализуют вызов /cc с помощью конечного автомата, который записывает локальное состояние в кучу. Именно это и делает C# 5.0 (или, вернее, итератор C# 2.0). Они реализовали call/cc, абстракция, которую они придумали, довольно элегантна.