Ответ 1
await
действительно не совсем то же самое, что call/cc
.
Тип полностью фундаментального call/cc
, о котором вы думаете, действительно должен был бы сохранить и восстановить весь стек вызовов. Но await
- это просто преобразование времени компиляции. Он делает что-то подобное, но не использует реальный стек вызовов.
Представьте, что у вас есть функция async, содержащая выражение ожидания:
async Task<int> GetInt()
{
var intermediate = await DoSomething();
return calculation(intermediate);
}
Теперь представьте, что функция, которую вы вызываете через await
, содержит выражение await
:
async Task<int> DoSomething()
{
var important = await DoSomethingImportant();
return un(important);
}
Теперь подумайте, что произойдет, когда DoSomethingImportant()
закончит и его результат будет доступен. Элемент управления возвращается к DoSomething()
. Тогда DoSomething()
заканчивается и что происходит тогда? Элемент управления возвращается к GetInt()
. Поведение точно так же, как если бы GetInt()
находился в стеке вызовов. Но это действительно не так; вы должны использовать await
для каждого вызова, который вы хотите моделировать таким образом. Таким образом, стек вызовов поднимается в стек мета-вызовов, который реализуется в awaiter.
То же самое, кстати, верно для yield return
:
IEnumerable<int> GetInts()
{
foreach (var str in GetStrings())
yield return computation(str);
}
IEnumerable<string> GetStrings()
{
foreach (var stuff in GetStuffs())
yield return computation(stuff);
}
Теперь, если я вызываю GetInts()
, то, что я возвращаю, является объектом, который инкапсулирует текущее состояние выполнения GetInts()
(так что вызов MoveNext()
на нем возобновляет операцию, где он был остановлен). Этот объект сам содержит итератор, который выполняет итерацию через GetStrings()
и вызывает MoveNext()
. Таким образом, реальный стек вызовов заменяется иерархией объектов, которые каждый раз воссоздают правильный стек вызовов с помощью серии вызовов MoveNext()
на следующем внутреннем объекте.