概念モデル
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 呼び出しに関して次の要件を挙げています。- 非同期呼び出しよりも同期呼び出しを優先する
- ポーリング付きの非同期を提供する
- コールバック付きの非同期を提供する
- 非同期処理を実行するスレッドを制御できるようにする
- 完了コールバックが実行されるスレッドを制御できるようにする
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 があります。
Internal フィールドはシステムが使用するため、変更してはなりません。この構造体のユーザーが設定可能なフィールドは、非同期操作中に変更してはいけません。XAsyncBlock は、非同期操作のライフタイムの間、メモリ上に残っている必要があります。XAsyncBlock が動的に割り当てられている場合、それを削除できる最も早い時点は完了コールバックです。
XAsyncBlock に加え、次のようにいくつかのヘルパー API が用意されています。
非同期 API の使用方法
まず、次のコード例の同期 API を見てみましょう。作業ディスパッチの制御
先の呼び出しで、どのスレッドが非同期作業を行ったのでしょうか? どのスレッドが完了コールバックを呼び出したのでしょうか? それは XAsyncBlock に割り当てられたタスク キューによって決まります。 タスク キューには 2 つの「ポート」、ワーク ポート と 完了ポート があります。各ポートには、そのポートにキューされたコールバックがどのように処理されるかを決定するディスパッチ モードがあります。次のようにいくつかのディスパッチ モードがあります。- スレッド プール: スレッド プール キューにキューされたコールバックは、システム スレッド プールで実行されます。スレッド プールは、呼び出しを並列に呼び出し、スレッド プール スレッドが利用可能になるたびにキューから順番に呼び出しを取り出して実行します。
- シリアル化スレッド プール: コールバックはスレッド プールにキューされ実行されますが、一度に 1 つずつ実行されます。
- 手動: 手動キューにキューされたコールバックは、自動的にはディスパッチされません。開発者が任意のスレッドでディスパッチする必要があります。
- 即時: 即時ディスパッチ モードはキューを行いません。コールバックを送信したスレッドで直ちに呼び出しを実行します。
通知
通知には終わりがなく、何度も呼び出される可能性があります。通知は、非同期呼び出しの要件のサブセットをサポートする必要があります。- ポーリング付きの非同期
- コールバック付きの非同期
- コールバックが行われるスレッドの制御
- 呼び出し固有のパラメーター、タスク キュー、オプションの void コンテキスト、および強く型付けされたコールバック ポインターを受け取る Register メソッド。最後のパラメーターはトークンを返す out パラメーターです。
- 呼び出し固有のコンテキストとトークンを受け取る Unregister メソッド。
- ポーリングは、通知コールバックとは関係のない別のメソッドを追加することでサポートされます。
非同期ライブラリ
非同期パターンをサポートする一貫した API の作成を容易にするため、API の「非同期の配管処理」を実装するために使用できるライブラリを提供しています。ライブラリの API は次のようになります。- 呼び出し元から渡された非同期ブロックを指定して XAsyncBegin を呼び出し、実装を提供するコールバックを提供します。
- 呼び出しの非同期処理を実行します。ワーカー スレッドで処理を実行する必要がある場合は XAsyncSchedule を呼び出します。OS の非同期プリミティブを使用して、それらのプリミティブを time-critical-safe にとどまる程度に高速にセットアップできる場合は、そちらが推奨されます。
- ワーカー スレッドのコールバックから他の非同期処理を呼び出す必要がある場合は、ワーカーから E_PENDING を返すことができます。ワーカー内から XAsyncSchedule を呼び出して、追加の作業を再スケジュールすることもできます。
- すべての処理が完了したら、XAsyncComplete を呼び出します。
- 結果を返すために、XAsyncGetResult のまわりに強く型付けされたラッパーを用意します。
- 非同期呼び出しにデータ ペイロードがない場合は、XAsyncGetStatus のまわりに強く型付けされたラッパーを用意し、XAsyncComplete に必要なバッファー サイズとしてゼロを渡す必要があります。
- Begin 非同期プロバイダーは、XAsyncBegin の中でこのオペコードで呼び出されます。プロバイダーがこのオペコードを実装する場合は、XAsyncSchedule を呼び出すか外部の手段を通じて、非同期タスクを開始する必要があります。このコールバックは XAsyncBegin の呼び出しチェーンで同期的に呼び出されるため、決してブロックしないようにしてください。
- DoWork タスク キューを使用して非同期処理をスケジュールするために XAsyncSchedule が呼び出された場合に呼ばれます。プロバイダー関数は必要な処理をすべて実行します。完了時には、結果コードとデータ ペイロード サイズを指定して XAsyncComplete を呼び出します (呼び出しにデータ ペイロードがない場合はゼロも可)。さらに非同期処理を行う必要がある場合は、プロバイダーはその処理をスケジュールし、E_PENDING を返す必要があります。
- GetResult 呼び出しの結果を取得するために呼び出されます。データ サイズは呼び出し完了時に XAsyncComplete に渡されているため、ここでの引数チェックは不要です。すべてのバッファーとバッファー サイズは、ライブラリによって既に検証されています。
- Cancel ユーザーが非同期呼び出しをキャンセルすると呼び出されます。呼び出しをキャンセルできる場合は、それをキャンセルし、結果コードとして E_ABORT を指定して XAsyncComplete を呼び出します。
- Cleanup 呼び出しが完全に終了し、プロバイダーが任意の動的メモリを削除できる時に呼び出されます。
