Skip to main content
Microsoft Game Development Kit (GDK) は、XBOX One ERA プログラミング モデルの一部として実装された非同期パターンに関してゲーム開発者から寄せられたフィードバックに対応する、新しい非同期 API のパターンを実装しています。この新しいパターンが、一般的なゲーム アーキテクチャに統合しやすく、ゲーム開発者が求めている高度な制御を提供することを目標としています。このトピックでは、その設計パターンについて説明し、非同期パターンを実装するために使用できるライブラリの提案を紹介します。

概念モデル

Microsoft Game Development Kit (GDK) における非同期プログラミングは、大きく分けて 2 つの主要コンポーネント (タスクとタスク キュー) に分かれています。ライブラリにはさらに多くの機能がありますが、概念モデル全体はこれら 2 つの主要コンポーネントを利用しています。 タスクとは、開始し、状態を確認し、必要に応じてキャンセルし、完了し、完了情報を返すことができる一連の非同期作業です。Microsoft Game Development Kit (GDK) モデルにおけるタスクは、ワーク コールバックと完了コールバックの 2 つの本体で構成されます。これにより、完全並列処理や、並列処理と単一スレッド完了の組み合わせなど、より高度な制御が可能になります。 タスク キューは、後で実行するためにワーク コールバックと完了コールバックの両方をエンキューするコンテナーです。タスク キューには、ワーク コールバックと完了コールバックを別々に扱う 2 つの内部キュー (ポートと呼びます) があります。これらはワーク ポートと完了ポートと呼ばれます。 図 1. タスクとタスク キューの図 タスク キューの各ポートは、作成時 に個別に構成することで、異なるコールバック実行動作を作成できます。たとえば、ワーク ポートを非同期に構成し、完了ポートをメイン スレッドでシリアルに実行するように構成することができます。マニュアル設定により、実行動作を完全に制御することもできます。ポート構成モードについては 以下で説明 しています。 非同期タスクが開始されると、コールバックはただちにタスク キューにエンキューされるわけではありません。非同期プロバイダー が状態変化を処理し、作業がエンキューされてディスパッチされてから完了コールバックがエンキューされ、ディスパッチされるようにします。 タスク キュー自身は、直接的にスレッド化を扱いません。代わりに、そのポートを ディスパッチ する外部呼び出しに依存します。外部呼び出しがスレッド化と並列実行の動作を決定します。タスク キュー自体は完全にスレッド セーフです。 図 2. 複数のスレッドにディスパッチされているポート 基本的にはこれだけです。タスクのコールバックはタスク キューのワーク ポートと完了ポートにエンキューされ、そのタスク キューがそれらのコールバックを何らかの方法でディスパッチします。API には、タスク キューの管理、コールバックの状態確認、作業データの追跡、カスタムタスク処理の作成など、豊富な機能が含まれています。 Microsoft Game Development Kit (GDK) の非同期 API 呼び出しは常にワーク コールバックを内部的に実装し、完了コールバックは常にオプションです。Microsoft Game Development Kit (GDK) の非同期呼び出し以外で使用する場合は、ワーク コールバックを指定する必要があります。

要件

ゲーム開発者は、API 呼び出しに関して次の要件を挙げています。
  1. 非同期呼び出しよりも同期呼び出しを優先する
  2. ポーリング付きの非同期を提供する
  3. コールバック付きの非同期を提供する
  4. 非同期処理を実行するスレッドを制御できるようにする
  5. 完了コールバックが実行されるスレッドを制御できるようにする

API の種類

Microsoft Game Development Kit (GDK) は、API 設計において非常に率直であることを目指しています。ゲーム開発者は、ハードウェアの使用を最大化するようにコードを細かく調整する専門家です。可能な限り制御を彼らに委ねます。API の実装は次のような種類に分類されます。
  • タイム センシティブ セーフ: タイム センシティブ セーフな API とは、タイム センシティブなスレッドで呼び出せる API のことです。通常、この API は些細なものや非常に高速なものを意味しますが、鍵となる概念は、API のパフォーマンス特性が 一貫している ことです。これらは常に同期であり、非同期版が必要になることはありません。これらの API は「time-sensitive-safe」として文書化する必要があります。
  • タイム センシティブ セーフでない: これらの API はレンダー スレッドから呼び出すのは安全ではありません。パフォーマンス特性は大きく変動する可能性があります。ほとんどの API はこのカテゴリに該当します。
  • 非同期: これらの API は、Web サービス呼び出しなど、本質的に非同期です。このトピックで説明する非同期パターンを使用します。非同期 API は、XBOX One ERA プログラミング モデルほど Microsoft Game Development Kit (GDK) には多くありません。非同期 API は一般に長時間実行され、キャンセル可能です。特定の使用ケースを除き、非同期 API には not-time-critical-safe な同期版があります。非同期 API を呼び出す動作は常に time-critical-safe であるべきです。
  • 通知: 通知は本質的に周期的であり、定義された終わりがありません。これらは非同期 API に関連していますが、周期的性質のため、開発者に対しては異なる見た目と振る舞いをすべきです。通知の登録は常に time-critical-safe であるべきです。

非同期 API パターン

Microsoft Game Development Kit (GDK) では、Microsoft Game Development Kit (GDK) コンポーネントが一貫した非同期サポートを提供するために使える、汎用的な非同期 API パターンを導入しています。その中核には OVERLAPPED に類似した構造体である XAsyncBlock があります。
XAsyncBlock は呼び出し元が提供する構造体です。呼び出し元は次の表に示すオプション フィールドをこの構造体に入力します。 Internal フィールドはシステムが使用するため、変更してはなりません。この構造体のユーザーが設定可能なフィールドは、非同期操作中に変更してはいけません。XAsyncBlock は、非同期操作のライフタイムの間、メモリ上に残っている必要があります。XAsyncBlock が動的に割り当てられている場合、それを削除できる最も早い時点は完了コールバックです。 XAsyncBlock に加え、次のようにいくつかのヘルパー API が用意されています。
XAsyncGetStatus は非同期呼び出しの状態を返します。呼び出しが開始すると、この状態は E_PENDING になります。完了すると S_OK または特定のエラーに変わります。呼び出しがキャンセルされた場合は E_ABORT を返します。 XAsyncGetResultSize は、呼び出しの結果を取得するために必要なバッファーのサイズを返します。実際に結果を取得する API は非同期呼び出しごとにカスタマイズされています。 XAsyncCancel は呼び出しをキャンセルするために使用できます。キャンセルの動作はキャンセルされる操作次第で、同期的、非同期的、またはまったくキャンセルされない場合があります。操作がキャンセルされると、XAsyncGetResultXAsyncGetResultSize、または XAsyncGetStatus は E_ABORT を返します。キャンセルされた呼び出しは、XAsyncBlockXAsyncCompletionRoutine パラメーターにシグナルを送り、そのコールバックを呼び出します。 XAsyncRun は、任意のコードを非同期に実行できるヘルパー メソッドです。

非同期 API の使用方法

まず、次のコード例の同期 API を見てみましょう。
この API は Web サービスを呼び出して、まだ残っているセーブ ゲーム ストレージの量を判別します。非同期サポートを追加するために、次のように新しい API のペアを宣言します。
XGameSaveGetRemainingQuotaAsync は非同期呼び出しが開始された場合に S_OK を返します (この API は非同期専用なので E_PENDING を返す意味はありません)。XGameSaveGetRemainingQuotaResult は、呼び出しが完了するまで E_PENDING を返します。 これを実際に使用する例を次に示します。
XAsyncBlocks はすべて、非同期呼び出しがどこでどのように実行されるかを制御するタスク キュー (後述) を必要とします。指定されなかった場合は、プロセス全体のタスク キューが使用されます。 XAsyncBlock は非同期呼び出しの間、メモリ上に残る必要があることに注意してください。この例では、動的に割り当てられ、完了コールバックで削除されます。また、グローバル変数やメンバー変数として保存することもできます。同じ XAsyncBlock を同時に複数の非同期呼び出しに使用すると、動作は未定義になります。 XGameSaveGetRemainingQuotaResult は非同期呼び出しのサイクルを完了させます。非同期ブロック内の内部データを解放するので、ブロックを新しい呼び出しに使用できるようになります。それ以降の XGameSaveGetRemainingQuotaResult の呼び出しは失敗します。XGameSaveGetRemainingQuotaAsyncXGameSaveGetRemainingQuotaResult は非同期ブロック内でペアになっています。ある非同期呼び出しに対して別の Result API を混在させるとエラーが発生します。 非同期呼び出しにデータ ペイロードがなく、HRESULT ステータスだけが重要な場合は、次のように非同期ブロックのみを受け取る Result メソッドを定義します。

作業ディスパッチの制御

先の呼び出しで、どのスレッドが非同期作業を行ったのでしょうか? どのスレッドが完了コールバックを呼び出したのでしょうか? それは XAsyncBlock に割り当てられたタスク キューによって決まります。 タスク キューには 2 つの「ポート」、ワーク ポート完了ポート があります。各ポートには、そのポートにキューされたコールバックがどのように処理されるかを決定するディスパッチ モードがあります。次のようにいくつかのディスパッチ モードがあります。
  • スレッド プール: スレッド プール キューにキューされたコールバックは、システム スレッド プールで実行されます。スレッド プールは、呼び出しを並列に呼び出し、スレッド プール スレッドが利用可能になるたびにキューから順番に呼び出しを取り出して実行します。
  • シリアル化スレッド プール: コールバックはスレッド プールにキューされ実行されますが、一度に 1 つずつ実行されます。
  • 手動: 手動キューにキューされたコールバックは、自動的にはディスパッチされません。開発者が任意のスレッドでディスパッチする必要があります。
  • 即時: 即時ディスパッチ モードはキューを行いません。コールバックを送信したスレッドで直ちに呼び出しを実行します。
既定のプロセス タスク キューが構成されているため、ワーク ポートと完了ポートの両方がシステム スレッド プール経由でディスパッチされます。このプロセス タスク キューは、XAsyncBlock にキュー パラメーターが渡されない場合に使用されます。ゲームはプロセス タスク キューを無効にして、XAsyncBlock にキューを渡すように必須にすることもできます。 私たちは、非同期処理と完了コールバックがいつどこで実行されるかを完全に制御するために、多くの開発者が手動ディスパッチ モードを選択することを想定しています。 タスク キューの詳細については、非同期タスク キューの設計 を参照してください。

通知

通知には終わりがなく、何度も呼び出される可能性があります。通知は、非同期呼び出しの要件のサブセットをサポートする必要があります。
  1. ポーリング付きの非同期
  2. コールバック付きの非同期
  3. コールバックが行われるスレッドの制御
通知はタスク キューを使用して、開発者がコールバック スレッドを制御できるようにしますが、それ以外は非同期ブロックを使用しません。Register および Unregister メソッドを備えた標準的なイベントに近い見た目と動作をするように設計されています。
  • 呼び出し固有のパラメーター、タスク キュー、オプションの void コンテキスト、および強く型付けされたコールバック ポインターを受け取る Register メソッド。最後のパラメーターはトークンを返す out パラメーターです。
  • 呼び出し固有のコンテキストとトークンを受け取る Unregister メソッド。
  • ポーリングは、通知コールバックとは関係のない別のメソッドを追加することでサポートされます。
Windows メッセージを取得する次のような例を見てみましょう。
この例では、UnregisterMessageAvailable は最後の “wait” パラメーターを取り、bool を返すことに注意してください。これにより、呼び出しが実行中である間に登録解除をどのように処理するかを呼び出し元が決定できます。

非同期ライブラリ

非同期パターンをサポートする一貫した API の作成を容易にするため、API の「非同期の配管処理」を実装するために使用できるライブラリを提供しています。ライブラリの API は次のようになります。
この API は、単一のコールバックと、API が呼ばれている理由を示す操作値を組み合わせて使用します。また、呼び出しの進行に伴って埋められる単一のデータ構造もあります。この API を使用するには、以下の手順を実行します。
  1. 呼び出し元から渡された非同期ブロックを指定して XAsyncBegin を呼び出し、実装を提供するコールバックを提供します。
  2. 呼び出しの非同期処理を実行します。ワーカー スレッドで処理を実行する必要がある場合は XAsyncSchedule を呼び出します。OS の非同期プリミティブを使用して、それらのプリミティブを time-critical-safe にとどまる程度に高速にセットアップできる場合は、そちらが推奨されます。
  3. ワーカー スレッドのコールバックから他の非同期処理を呼び出す必要がある場合は、ワーカーから E_PENDING を返すことができます。ワーカー内から XAsyncSchedule を呼び出して、追加の作業を再スケジュールすることもできます。
  4. すべての処理が完了したら、XAsyncComplete を呼び出します。
  5. 結果を返すために、XAsyncGetResult のまわりに強く型付けされたラッパーを用意します。
  6. 非同期呼び出しにデータ ペイロードがない場合は、XAsyncGetStatus のまわりに強く型付けされたラッパーを用意し、XAsyncComplete に必要なバッファー サイズとしてゼロを渡す必要があります。
非同期プロバイダー コールバックは、次の操作で呼び出されます。
  • Begin 非同期プロバイダーは、XAsyncBegin の中でこのオペコードで呼び出されます。プロバイダーがこのオペコードを実装する場合は、XAsyncSchedule を呼び出すか外部の手段を通じて、非同期タスクを開始する必要があります。このコールバックは XAsyncBegin の呼び出しチェーンで同期的に呼び出されるため、決してブロックしないようにしてください。
  • DoWork タスク キューを使用して非同期処理をスケジュールするために XAsyncSchedule が呼び出された場合に呼ばれます。プロバイダー関数は必要な処理をすべて実行します。完了時には、結果コードとデータ ペイロード サイズを指定して XAsyncComplete を呼び出します (呼び出しにデータ ペイロードがない場合はゼロも可)。さらに非同期処理を行う必要がある場合は、プロバイダーはその処理をスケジュールし、E_PENDING を返す必要があります。
  • GetResult 呼び出しの結果を取得するために呼び出されます。データ サイズは呼び出し完了時に XAsyncComplete に渡されているため、ここでの引数チェックは不要です。すべてのバッファーとバッファー サイズは、ライブラリによって既に検証されています。
  • Cancel ユーザーが非同期呼び出しをキャンセルすると呼び出されます。呼び出しをキャンセルできる場合は、それをキャンセルし、結果コードとして E_ABORT を指定して XAsyncComplete を呼び出します。
  • Cleanup 呼び出しが完全に終了し、プロバイダーが任意の動的メモリを削除できる時に呼び出されます。
非同期プロバイダーは、必要な操作のみを実装するだけで済みます。たとえば、クリーンアップを持たないキャンセル不能な非同期 IO は GetResult のみを実装すればよいのです。 以下は、階乗を非同期に実装する FactorialAsync メソッドの例です。

リファレンス API ドキュメント

関連項目

非同期プログラミングの設計目標と改善 非同期タスク キューの設計
最終更新日 2026年8月24日