async と await を使用して、1 つのスレッドでのみ実行され、サイクルを無駄にせず (これはゲーム コードです)、コルーチンの呼び出し元に例外をスローすることができる (これはコルーチン自体)?
バックグラウンド
(ペット ゲーム プロジェクト) Lua コルーチン AI コード ( LuaInterfaceを介して C# でホストされている) を C# コルーチン AI コードに置き換えることを実験しています。
• 各 AI (モンスターなど) を独自のコルーチン (またはネストされたコルーチンのセット) として実行し、メイン ゲーム スレッドが各フレーム (1 秒あたり 60 回) の一部またはすべてを「シングル ステップ」にすることを選択できるようにします。他のワークロードに依存する AI。
• しかし、読みやすさとコーディングの容易さのために、重要な作業を行った後にタイム スライスを "生成" することだけがスレッドを認識するように AI コードを記述したいと考えています。そして、私は mid メソッドを「譲り」、すべてのローカルなどをそのままにして次のフレームを再開できるようにしたいと考えています (await で期待されるように)。
• 私は IEnumerable<> と yield return を使用したくありません。これは、一部は見苦しさ、一部は報告された問題に対する迷信、特に async と await の方が論理的に適合しているように見えるためです。
論理的に言えば、メイン ゲームの疑似コードは次のとおりです。
void MainGameInit()
{
foreach (monster in Level)
Coroutines.Add(() => ASingleMonstersAI(monster));
}
void MainGameEachFrame()
{
RunVitalUpdatesEachFrame();
while (TimeToSpare())
Coroutines.StepNext() // round robin is fine
Draw();
}
AI の場合:
void ASingleMonstersAI(Monster monster)
{
while (true)
{
DoSomeWork(monster);
<yield to next frame>
DoSomeMoreWork(monster);
<yield to next frame>
...
}
}
void DoSomeWork(Monster monster)
{
while (SomeCondition())
{
DoSomethingQuick();
DoSomethingSlow();
<yield to next frame>
}
DoSomethingElse();
}
...
アプローチ
Windows デスクトップ (.NET 4.5) 用の VS 2012 Express では、Jon Skeet の優れたEduasync パート 13のサンプル コードをそのまま使用しようとしています。
そのソースは、このリンクから入手できます。提供された AsyncVoidMethodBuilder.cs は、mscorlib のリリース バージョンと競合するため (問題の一部である可能性があります) 使用しません。.NET 4.5 のリリース バージョンで必要なため、提供された Coordinator クラスを System.Runtime.CompilerServices.INotifyCompletion を実装するものとしてマークする必要がありました。
それにもかかわらず、サンプル コードを実行するためのコンソール アプリケーションの作成はうまく機能し、まさに私が望んでいたものです。つまり、IEnumerable<> ベースのコルーチンの見苦しさなしに、"yield" として await を使用した単一スレッドでの協調マルチスレッド化です。
次に、サンプルの FirstCoroutine 関数を次のように編集します。
private static async void FirstCoroutine(Coordinator coordinator)
{
await coordinator;
throw new InvalidOperationException("First coroutine failed.");
}
Main() を次のように編集します。
private static void Main(string[] args)
{
var coordinator = new Coordinator {
FirstCoroutine,
SecondCoroutine,
ThirdCoroutine
};
try
{
coordinator.Start();
}
catch (Exception ex)
{
Console.WriteLine("*** Exception caught: {0}", ex);
}
}
例外がキャッチされることを素朴に望んでいました。代わりに、そうではありません。この「シングル スレッド」コルーチンの実装では、スレッド プール スレッドでスローされるため、キャッチされません。
このアプローチに対する試みられた修正
周りを読んで、問題の一部を理解しました。コンソール アプリケーションには SynchronizationContext がありません。また、ある意味で async void は結果を伝播することを意図していないこともわかりましたが、ここでそれについて何をすべきか、またはタスクの追加がシングルスレッドの実装でどのように役立つかはわかりません。
コンパイラで生成された FirstCoroutine のステート マシン コードから、MoveNext() 実装を介して例外が AsyncVoidMethodBuilder.SetException() に渡されることがわかります。これにより、同期コンテキストの欠如が検出され、ThrowAsync() が呼び出され、最終的にスレッド プールになります。私が見ているようにスレッド。
ただし、SynchronisationContext を単純にアプリに移植しようとする私の試みは成功していません。これを追加して、Main() の開始時に SetSynchronizationContext() を呼び出し、Coordinator の作成全体と呼び出しを AsyncPump().Run() でラップしてみました。その中で Debugger.Break() (ブレークポイントではありません) を実行できます。クラスの Post() メソッドを調べて、例外がここにあることを確認します。しかし、そのシングルスレッドの同期コンテキストは単純に連続して実行されます。例外を呼び出し元に伝播する作業を行うことはできません。そのため、Coordinator シーケンス全体 (およびその catch ブロック) が完了してほこりを払った後、例外が発生します。
Post() メソッドが指定された Action をただちに実行するだけの独自の SynchronizationContext を派生させるという、さらにナイーブなアプローチを試みました。これは有望に見えました (そのコンテキストがアクティブな状態で呼び出された複雑なコードに対する恐ろしい影響を伴うことは間違いありませんか?) が、これは生成されたステート マシン コードに違反します。
部分的な「解決策」、おそらく賢明ではありませんか?
熟考を続けると、部分的な「解決策」がありますが、暗闇の中で釣りをしているので、影響が何であるかはわかりません.
Jon Skeet の Coordinator をカスタマイズして、Coordinator 自体への参照を持つ独自の SynchronizationContext 派生クラスをインスタンス化できます。上記のコンテキストが Send() または Post() コールバック (AsyncMethodBuilderCore.ThrowAsync() などによる) を要求されると、代わりにこれをアクションの特別なキューに追加するようコーディネーターに要求します。
コーディネーターは、任意のアクション (コルーチンまたは非同期継続) を実行する前に、これを現在のコンテキストとして設定し、その後、以前のコンテキストを復元します。
コーディネーターの通常のキューで任意のアクションを実行した後、特別なキューですべてのアクションを実行することを主張できます。これは、AsyncMethodBuilderCore.ThrowAsync() によって、関連する継続が途中で終了した直後に例外がスローされることを意味します。(AsyncMethodBuilderCore によってスローされた例外から元の例外を抽出するために、まだいくつかの釣りが必要です。)
ただし、カスタム SynchronizationContext の他のメソッドはオーバーライドされず、最終的に自分が何をしているのかについてまともな手がかりがないため、複雑な (特に async または Task指向、または真のマルチスレッド?) 当然のことながら、コルーチンによって呼び出されるコード?