それをもっと簡単なものに分解しましょう:
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を呼び出して」と伝えて戻ります。
10分の1秒後にタスクが完了し、メッセージループがhelper1のDoItを呼び出します。helper1の状態はAFTERSOMETHINGSTATEであるため、gotoを取得してGoを呼び出します。それはhelper2を作成し、その上でDoItを呼び出します。これにより、Something()が呼び出され、タスクが返され、そのタスクの待機者が作成され、待機者に「完了したら、helper2でDoItを呼び出します」と通知され、helper1のDoItに制御が戻ります。これはSomethingElseを呼び出し、そのタスクを待機し、「他のことを行ったら、helper1のDoItを呼び出します」と伝えます。その後、戻ります。
これで、2つのタスクが未解決になり、スタックにコードがありません。タスクの1つが最初に完了します。SomethingElseタスクが最初に完了したとします。メッセージループはhelper1.DoIt()を呼び出し、すぐに戻ります。Helper1はガベージになりました。
その後、メッセージループはhelper2.DoIt()を呼び出し、AFTERSOMETHINGに分岐します。ここでGo()が呼び出され、helper3...が作成されます。
いいえ、ここには無制限の再帰はありません。Goが実行されるたびに、Something()を非同期的に開始するまで実行され、呼び出し元に戻ります。「何か」の後のものへの呼び出しは後で起こります。「Go」は一度に一度だけスタックにあります。