Давайте разберем это на что-то более простое:
async static void Go()
{
await Something();
Go();
await SomethingElse();
}
Как компилятор справляется с этим?
В основном это выглядит примерно так:
class HelperClass
{
private State state = STARTSTATE;
public void DoIt()
{
if (state == STARTSTATE) goto START;
if (state == AFTERSOMETHINGSTATE) goto AFTERSOMETHING;
if (state == AFTERSOMETHINGELSESTATE) goto AFTERSOMETHINGELSE;
START:
{
state = AFTERSOMETHINGSTATE;
var awaiter = Something().MakeAnAwaiter();
awaiter.WhenDoneDo(DoIt);
return;
}
AFTERSOMETHING:
{
Go();
state = AFTERSOMETHINGELSESTATE;
var awaiter = SomethingElse().MakeAnAwaiter();
awaiter.WhenDoneDo(DoIt);
return;
}
AFTERSOMETHINGELSE:
return;
}
static void Go()
{
var helper = new HelperClass();
helper.DoIt();
}
Теперь все, что вам нужно запомнить, - это то, что после завершения каждой асинхронной операции запланирован повторный вызов DoIt в цикле сообщений (конечно, в соответствующем экземпляре помощника).
Так что же происходит? Проработай это. Вы звоните Go в первый раз. Это делает помощника номер один и вызывает DoIt. Это вызывает Something (), возвращает задачу назад, делает ожидающего ее, сообщает ожидающему «когда вы закончите, вызовите helper1.DoIt» и возвращает.
Десятая секунда спустя задача завершается, и цикл сообщений вызывает DoIt helper1. Состояние helper1 - AFTERSOMETHINGSTATE, поэтому мы берем goto и вызываем Go. Это делает helper2 и вызывает DoIt на этом. Это вызывает Something (), возвращает задачу назад, делает ее ожидающей, сообщает ожидающему «когда вы закончите, вызовите DoIt для helper2» и возвращает управление обратно в DoIt helper1. Это вызывает SomethingElse, делает ожидание для этой задачи и говорит ему "когда вы закончите делать что-то еще, вызовите DoIt helper1". Затем он возвращается.
Теперь у нас есть две нерешенные задачи и нет кода в стеке. Одна из задач будет выполнена в первую очередь. Предположим, что задача SomethingElse завершается первой. Цикл сообщений вызывает helper1.DoIt (), который немедленно возвращается. Helper1 теперь фигня.
Позже цикл сообщений вызывает helper2.DoIt () и переходит к AFTERSOMETHING. Теперь вызывается Go (), который создает helper3 ...
Так что нет, здесь нет неограниченной рекурсии. Каждый раз, когда Go запускается, он запускается до асинхронного запуска Something () и затем возвращается к своему вызывающему. Призыв к вещам после «чего-то» происходит позже. «Go» находится в стеке только один раз за раз.