11

私はマルチプレイヤー ゲーム サーバーを作成しており、新しい C# の async/await 機能が役立つ方法を検討しています。サーバーのコアは、ゲーム内のすべてのアクターを可能な限り高速に更新するループです。

while (!shutdown)
{
    foreach (var actor in actors)
        actor.Update();

    // Send and receive pending network messages
    // Various other system maintenance
}

このループは、何千ものアクターを処理し、ゲームをスムーズに実行し続けるために毎秒複数回更新するために必要です。一部のアクターは、データベースからデータをフェッチするなど、更新機能で低速のタスクを実行することがあります。これは、非同期を使用したい場合です。このデータが取得されると、アクターはゲームの状態を更新しようとしますが、これはメイン スレッドで行う必要があります。

これはコンソール アプリケーションであるため、保留中のデリゲートをメイン ループにディスパッチできる SynchronizationContext を作成する予定です。これにより、これらのタスクが完了するとゲームが更新され、未処理の例外がメイン ループにスローされるようになります。私の質問は、非同期更新関数をどのように書くのですか? これは非常にうまく機能しますが、async void を使用しないという推奨事項を破っています。

Thing foo;

public override void Update()
{
    foo.DoThings();

    if (someCondition) {
        UpdateAsync();
    }
}

async void UpdateAsync()
{
    // Get data, but let the server continue in the mean time
    var newFoo = await GetFooFromDatabase();

    // Now back on the main thread, update game state
    this.foo = newFoo;
}

Update() を非同期にして、タスクをメイン ループに戻すこともできますが、次のようになります。

  • それを決して使用しない何千もの更新にオーバーヘッドを追加したくありません。
  • メインループでも、タスクを待機してループをブロックしたくありません。
  • タスクを待機すると、待機中のスレッドで完了する必要があるため、とにかくデッドロックが発生します。

待ちきれないこれらすべてのタスクをどうすればよいですか? それらがすべて完了したことを知りたいのは、サーバーをシャットダウンするときだけですが、おそらく数週間分の更新によって生成されたすべてのタスクを収集したくありません.

4

3 に答える 3

9

私の理解では、その核心はあなたが望むことです:

while (!shutdown)
{
    //This should happen immediately and completions occur on the main thread.
    foreach (var actor in actors)
        actor.Update(); //includes i/o bound database operations

    // The subsequent code should not be delayed
   ...
}

メイン コンソール スレッドで while ループが実行されている場所。これはタイトなシングル スレッド ループです。foreach を並行して実行することもできますが、実行時間が最も長いインスタンス (データベースからデータを取得するための i/o バウンド操作) を待機することになります。

await async は、このループ内では最適なオプションではありません。これらの I/O データベース タスクをスレッド プールで実行する必要があります。スレッド プールでは、async await はプール スレッドを解放するのに役立ちます。

したがって、次の質問は、これらの補完をメインスレッドに戻す方法です。メイン スレッドにメッセージ ポンプに相当するものが必要なようです。その方法については、この投稿を参照してください。whileループを通過するたびにメインスレッドでチェックする、並べ替えの完了キューを持つことができます。これを行うには、並行データ構造の 1 つを使用して、すべてスレッド セーフにし、設定が必要な場合は Foo を設定します。

このアクターのポーリングとスレッド化を合理化する余地があるようですが、アプリの詳細を知らなければ、なんとも言えません。

いくつかのポイント: -

  • タスクの上位に Wait がない場合、メイン コンソール スレッドが終了し、アプリケーションも終了します。詳しくはこちらをご覧ください。

  • ご指摘のとおり、await async は現在のスレッドをブロックしませんが、await に続くコードは await の完了時にのみ実行されることを意味します。

  • 完了は、呼び出し元のスレッドで完了している場合と完了していない場合があります。同期コンテキストについては既に言及したので、詳細には触れません。

  • コンソール アプリで同期コンテキストが null です。詳しくはこちらをご覧ください。

  • 非同期は、実際にはファイアアンドフォーゲットタイプの操作用ではありません。

ファイア アンド フォーゲットでは、シナリオに応じて次のいずれかのオプションを使用できます。

  • Task.Run または Task.StartNew を使用します。違いについてはこちらをご覧ください。
  • 独自のスレッドプールで実行される長時間実行シナリオには、プロデューサー/コンシューマー タイプのパターンを使用します。

次のことに注意してください。

  • 生成されたタスク/スレッドで例外を処理する必要があること。観察していない例外がある場合は、それらの発生をログに記録するだけでも、これらを処理したい場合があります。観察されない例外に関する情報を参照してください。
  • これらの長時間実行タスクがキューにある、または開始している間にプロセスが終了すると、それらのタスクは実行されないため、これらの操作の状態を追跡する何らかの永続メカニズム (データベース、外部キュー、ファイル) が必要になる場合があります。

これらのタスクの状態を知りたい場合は、何らかの方法でそれらを追跡する必要があります。これは、メモリ内のリストであるか、独自のスレッド プールのキューをクエリするか、永続化メカニズムをクエリすることによって行われます。永続化メカニズムの優れた点は、クラッシュに対する回復力があり、シャットダウン中にすぐにシャットダウンして、再起動時に終了した場所を取得できることです (もちろん、これはタスクが実行されることがどれほど重要かによって異なります)。特定の時間枠)。

于 2013-09-10T03:31:02.823 に答える
0

このビデオまたはスライドを見ることを強くお勧めします: Microsoft Visual C# および Visual Basic で Async を使用するための 3 つの重要なヒント

私の理解では、このシナリオでおそらく何をすべきかは、 に戻ってきTask<Thing>UpdateAsync、おそらくUpdate.

メインループの外で「foo」を使用して非同期操作を実行している場合、将来の順次更新中に非同期部分が完了するとどうなりますか? すべての更新タスクが完了するのを待ってから、一度に内部状態を交換したいと本当に思っていると思います。

理想的には、すべての遅い (データベース) 更新を最初に開始してから、他の高速更新を実行して、セット全体ができるだけ早く準備されるようにすることをお勧めします。

于 2013-09-10T06:12:35.257 に答える