4

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指向、または真のマルチスレッド?) 当然のことながら、コルーチンによって呼び出されるコード?

4

1 に答える 1

2

面白いパズル。

問題

あなたが指摘したように、問題は、デフォルトでは void async メソッドを使用しているときにキャッチされる例外は、 を使用してキャッチされAsyncVoidMethodBuilder.SetException、次に を使用することAsyncMethodBuilderCore.ThrowAsync();です。いったんそこにあると、別のスレッドで(スレッドプールから)例外がスローされるため、面倒です。とにかく、この動作をオーバーライドする方法はないようです。

ただし、AsyncVoidMethodBuilderメソッドの非同期メソッド ビルダーですvoidTask非同期メソッドはどうですか?それは を介し​​て処理されAsyncTaskMethodBuilderます。このビルダーとの違いは、それを現在の同期コンテキストに伝搬する代わりにTask.SetException、例外がスローされたというタスクをユーザーに通知するために呼び出すことです。

解決策

Task返された非同期メソッドが返されたタスクに例外情報を格納することがわかっているので、コルーチンを task-returning-method に変換し、各コルーチンの最初の呼び出しから返されたタスクを使用して後で例外をチェックできます。(非同期メソッドを返す void/Task は同一であるため、ルーチンを変更する必要がないことに注意してください)。

Coordinatorこれには、クラスにいくつかの変更が必要です。まず、2 つの新しいフィールドを追加します。

private List<Func<Coordinator, Task>> initialCoroutines = new List<Func<Coordinator, Task>>();
private List<Task> coroutineTasks = new List<Task>();

initialCoroutinesは最初にコーディネーターに追加されたコルーチンをcoroutineTasks格納し、 の最初の呼び出しの結果として生じるタスクを格納しますinitialCoroutines

次に、Start() ルーチンは、新しいルーチンを実行し、結果を保存し、新しいアクションごとにタスクの結果を確認するように調整されています。

foreach (var taskFunc in initialCoroutines)
{
    coroutineTasks.Add(taskFunc(this));
}

while (actions.Count > 0)
{
    Task failed = coroutineTasks.FirstOrDefault(t => t.IsFaulted);
    if (failed != null)
    {
        throw failed.Exception;
    }
    actions.Dequeue().Invoke();
}

これにより、例外が元の呼び出し元に伝播されます。

于 2013-06-12T02:09:23.057 に答える