Я отвечу на ваши конкретные вопросы ниже, но вам, вероятно, стоит просто прочитать мои обширные статьи о том, как мы разработали yield и await.
https://blogs.msdn.microsoft.com/ericlippert/tag/continuation-passing-style/
https://blogs.msdn.microsoft.com/ericlippert/tag/iterators/
https://blogs.msdn.microsoft.com/ericlippert/tag/async/
Некоторые из этих статей уже устарели; сгенерированный код во многом отличается. Но они, безусловно, дадут вам представление о том, как это работает.
Кроме того, если вы не понимаете, как лямбда-выражения генерируются как классы закрытия, сначала поймите это . Вы не будете разбираться в асинхронности, если у вас нет лямбд.
Когда достигается ожидание, как среда выполнения узнает, какой фрагмент кода следует выполнить дальше?
await
создается как:
if (the task is not completed)
assign a delegate which executes the remainder of the method as the continuation of the task
return to the caller
else
execute the remainder of the method now
Вот в основном это. Ожидание - это просто фантастическое возвращение.
Как он узнает, когда он может возобновить работу с того места, на котором остановился, и как он запоминает, где?
Ну как это сделать без ожидания? Когда метод foo вызывает метод bar, мы каким-то образом запоминаем, как вернуться в середину foo, со всеми локальными переменными, активировавшими foo, независимо от того, что делает bar.
Вы знаете, как это делается на ассемблере. Запись активации для foo помещается в стек; он содержит ценности местных жителей. В момент вызова адрес возврата из foo помещается в стек. Когда bar готов, указатель стека и указатель инструкции сбрасываются туда, где они должны быть, и foo продолжает движение с того места, где он остановился.
Продолжение ожидания точно такое же, за исключением того, что запись помещается в кучу по той очевидной причине, что последовательность активаций не образует стек .
Делегат, который await дает в качестве продолжения задачи, содержит (1) число, которое является входом в таблицу поиска, которая дает указатель инструкции, которую вам нужно выполнить дальше, и (2) все значения локальных и временных переменных.
Там есть дополнительное снаряжение; например, в .NET запрещено переходить в середину блока try, поэтому вы не можете просто вставить адрес кода внутри блока try в таблицу. Но это бухгалтерские детали. По сути, запись активации просто перемещается в кучу.
Что происходит с текущим стеком вызовов, сохраняется ли он как-то?
Соответствующая информация в текущей записи активации никогда не помещается в стек; он выделяется из кучи с самого начала. (Ну, формальные параметры обычно передаются в стек или в регистры, а затем копируются в место в куче при запуске метода.)
Записи об активации вызывающих абонентов не сохраняются; ожидание, вероятно, вернется к ним, помните, так что с ними справятся нормально.
Обратите внимание, что это существенное различие между упрощенным стилем передачи продолжения в await и настоящими структурами вызова с текущим продолжением, которые вы видите в таких языках, как Scheme. На этих языках все продолжение, включая продолжение до вызывающих абонентов, фиксируется call-cc .
Что делать, если вызывающий метод вызывает другие методы до того, как он ожидает - почему стек не перезаписывается?
Эти вызовы методов возвращаются, и поэтому их записи активации больше не находятся в стеке на момент ожидания.
И как, черт возьми, среда выполнения могла бы пройти через все это в случае исключения и раскрутки стека?
В случае неперехваченного исключения исключение перехватывается, сохраняется внутри задачи и повторно генерируется при получении результата задачи.
Помните всю бухгалтерию, о которой я упоминал ранее? Правильная семантика исключений была огромной проблемой, позвольте мне вам сказать.
Когда доходность достигается, как среда выполнения отслеживает точку, где нужно поднять что-то? Как сохраняется состояние итератора?
Так же. Состояние локальных переменных перемещается в кучу, а число, представляющее инструкцию, с которой MoveNext
следует возобновить выполнение при следующем вызове, сохраняется вместе с локальными переменными.
И опять же, в блоке итератора есть множество приспособлений, чтобы убедиться, что исключения обрабатываются правильно.