XSAPI C API で非同期呼び出しを行う
非同期 API は、すばやくを制御を返し、非同期タスクを開始する API で、タスクが完了すると結果が返されます。
従来のゲームでは、完了コールバックを使用する場合に、どのスレッドが非同期タスクを実行し、どのスレッドが結果を返すかをほとんど制御していません。 ゲームによっては、スレッドの同期の必要性を回避するために、ヒープのセクションを 1 つのスレッドでのみ操作できるように設計されているものもあります。 完了コールバックが、ゲームが制御するスレッドから呼び出されていない場合、非同期タスクの結果で共有状態を更新するには、スレッドの同期が必要になります。
XSAPI C API が公開する新しい非同期 C API では、開発者は、XblSocialGetSocialRelationshipsAsync()、XblProfileGetUserProfileAsync()、XblAchievementsGetAchievementsForTitleIdAsync() などの非同期 API 呼び出しを行う際に直接スレッドを制御できます。
XblProfileGetUserProfileAsync API の基本的な呼び出しのサンプルを以下に示します。
XAsyncBlock* asyncBlock = new XAsyncBlock();
asyncBlock->queue = GlobalState()->queue;
asyncBlock->context = nullptr;
asyncBlock->callback = [](XAsyncBlock* asyncBlock)
{
XblUserProfile profile = { 0 };
HRESULT hr = XblProfileGetUserProfileResult(asyncBlock, &profile);
delete asyncBlock;
};
HRESULT hr = XblProfileGetUserProfileAsync(GlobalState()->xboxLiveContext, GlobalState()->xboxUserId, asyncBlock);
この呼び出しのパターンを理解するには、XAsyncBlock と XTaskQueueHandle の使用方法を理解する必要があります。
XAsyncBlock は、非同期タスクと完了コールバックに関連するすべての情報を保持します。
XTaskQueueHandle によって、非同期タスクを実行するスレッドと、XAsyncBlock の完了コールバックを呼び出すスレッドを決定することができます。
XAsyncBlock
XAsyncBlock を詳しく見てみましょう。 これは、次のように定義された構造体です。
typedef struct XAsyncBlock
{
/// <summary>
/// The queue to queue the call on
/// </summary>
XTaskQueueHandle queue;
/// <summary>
/// Optional context pointer to pass to the callback
/// </summary>
void* context;
/// <summary>
/// Optional callback that will be invoked when the call completes
/// </summary>
XAsyncCompletionRoutine* callback;
/// <summary>
/// Internal use only
/// </summary>
unsigned char internal[sizeof(void*) * 4];
};
XAsyncBlock には、次の要素が含まれます。
queue - 1 つの作業を実行する場所に関する情報を表すハンドルである XTaskQueueHandle。 これが設定されていない場合、既定のキューが使用されます。
context - データをコールバック関数に渡すことができるようにします。
callback - 非同期処理が完了した後に呼び出される、オプションのコールバック関数。 コールバックを指定しない場合、XAsyncGetStatus を使用して XAsyncBlock が完了するまで待機し、結果を取得します。
呼び出す非同期 API ごとにヒープで新しい XAsyncBlock を作成する必要があります。 XAsyncBlock は、XAsyncBlock の完了コールバックが呼び出されるまで存在している必要があります。その後、XAsyncBlock を削除できます。
重要:
XAsyncBlock は、非同期タスクが完了するまでメモリ内に存在している必要があります。 動的に割り当てられる場合、XAsyncBlock の完了コールバック内で削除できます。
非同期タスクの待機
非同期タスクが完了したことは、さまざまな方法で知ることができます。
- XAsyncBlock の完了コールバックが呼び出される。
- XAsyncGetStatus を呼び出して、true の場合は非同期タスクが完了するまで待機する。
XAsyncGetStatus では、非同期タスク は、XAsyncBlock の完了コールバックが実行された後、完了したと見なされますが、XAsyncBlock の完了コールバックは省略可能です。
非同期タスクが完了したら、結果を取得できます。
非同期タスクの結果の取得
結果を取得するために、ほとんどの非同期 API 関数には、非同期呼び出しの結果を受け取るための対応する [関数名]Result 関数があります。
コード例の場合、XblProfileGetUserProfileAsync には、対応する XblProfileGetUserProfileResult 関数があります。 この関数を使用して、関数の結果を取得し、それに応じて処理を実行できます。
結果の取得について詳しくは、それぞれの非同期 API 関数のドキュメントを参照してください。
XTaskQueueHandle
XTaskQueueHandle によって、非同期タスクを実行するスレッドと、XAsyncBlock の完了コールバックを呼び出すスレッドを決定することができます。
ディスパッチ モードを設定することによって、どのスレッドがこれらの処理を実行するかを制御できます。 次の 3 つのディスパッチ モードを使用できます。
手動 - 手動のキューは自動的にディスパッチされません。 開発者が、必要なスレッドに処理をディスパッチする必要があります。 これを使用して、非同期呼び出しの作業側またはコールバック側のいずれかを特定のスレッドに割り当てることができます。 これについては、以下で詳しく説明します。
スレッド プール - スレッド プールを使用してディスパッチします。 スレッド プールは、スレッド プールのスレッドが利用可能になると、キューから順番に実行する呼び出しを取り出して、呼び出しを並行して実行します。 これは、使用するのは最も簡単ですが、使用するスレッドの制御は最小限になります。
シリアル化スレッド プール - スレッド プールを使用してディスパッチします。 スレッド プールは、1 つのスレッド プールのスレッドが利用可能になると、キューから順番に実行する呼び出しを取り出して、呼び出しをシリアルに実行します。
即時-キューに入れられたが送信されたスレッドで作業をすぐにディスパッチします。
新しい XTaskQueueHandle を作成するには、XTaskQueueCreate を呼び出す必要があります。 例は以下のとおりです。
STDAPI XTaskQueueCreate(
_In_ XTaskQueueDispatchMode workDispatchMode,
_In_ XTaskQueueDispatchMode completionDispatchMode,
_Out_ XTaskQueueHandle* queue
) noexcept;
この関数は、2 つの XTaskQueueDispatchMode
パラメーターを取得します。
XTaskQueueDispatchMode
に有効な値は次の 3 つです。
/// <summary>
/// Describes how task queue callbacks are processed.
/// </summary>
enum class XTaskQueueDispatchMode : uint32_t
{
/// <summary>
/// Callbacks are invoked manually by XTaskQueueDispatch
/// </summary>
Manual,
/// <summary>
/// Callbacks are queued to the system thread pool and will
/// be processed in order by the thread pool across multiple thread
/// pool threads.
/// </summary>
ThreadPool,
/// <summary>
/// Callbacks are queued to the system thread pool and
/// will be processed one at a time.
/// </summary>
SerializedThreadPool,
/// <summary>
/// Callbacks are not queued at all but are dispatched
/// immediately by the thread that submits them.
/// </summary>
Immediate
};
workDispatchMode は、非同期操作を処理するスレッドのディスパッチ モードを決定し、completionDispatchMode は、非同期操作の完了を処理するスレッドのディスパッチ モードを決定します。
XTaskQueueHandle を作成したら、単にそれを XAsyncBlock に追加し、作業と完了関数のスレッド処理を制御します。 XTaskQueueHandle の使用が終了したら、通常、ゲームの終了時に XTaskQueueCloseHandle を使用して閉じることができます。
STDAPI_(void) XTaskQueueCloseHandle(
_In_ XTaskQueueHandle queue
) noexcept;
呼び出しサンプル:
XTaskQueueCloseHandle(queue);
手動での XTaskQueueHandle のディスパッチ
XTaskQueueHandle の作業または完了キューで、手動のキュー ディスパッチ モードを使用した場合、手動でディスパッチする必要があります。 XTaskQueueHandle は、次のように作業キューと完了キューの両方が手動でディスパッチするように設定されている場合のために作成されました。
XTaskQueueHandle queue = nullptr;
HRESULT hr = XTaskQueueCreate(
XTaskQueueDispatchMode::Manual,
XTaskQueueDispatchMode::Manual,
&queue);
XTaskQueueDispatchMode::Manual に割り当てられている作業をディスパッチするには、XTaskQueueDispatch 関数を使用してディスパッチする必要があります。
STDAPI_(bool) XTaskQueueDispatch(
_In_ XTaskQueueHandle queue,
_In_ XTaskQueuePort port,
_In_ uint32_t timeoutInMs
) noexcept;
呼び出しサンプル
HRESULT hr = XTaskQueueDispatch(queue, XTaskQueuePort::Completion, 0);
- queue - 作業をディスパッチする対象のキュー。
- port - XTaskQueuePort 列挙型のインスタンス。
- timeoutInMs - ミリ秒単位のタイムアウトを表す uint32_t。
XTaskQueuePort 列挙型によって定義されるコールバックには、2 つの種類があります。
/// <summary>
/// Declares which port of a task queue to dispatch or submit
/// callbacks to.
/// </summary>
enum class XTaskQueuePort : uint32_t
{
/// <summary>
/// Work callbacks
/// </summary>
Work,
/// <summary>
/// Completion callbacks after work is done
/// </summary>
Completion
};
XTaskQueueDispatch を呼び出す場合
キューが新しい項目を受信したことを確認するために、XTaskQueueRegisterMonitor を呼び出して、作業または完了をディスパッチする準備ができたことをコードで識別するためのイベント ハンドラーを設定できます。
STDAPI XTaskQueueRegisterMonitor(
_In_ XTaskQueueHandle queue,
_In_opt_ void* callbackContext,
_In_ XTaskQueueMonitorCallback* callback,
_Out_ XTaskQueueRegistrationToken* token
) noexcept;
XTaskQueueRegisterMonitor は次のパラメーターを受け取ります。
- queue - コールバックを送信している対象の非同期キュー。
- callbackContext - 送信コールバックに渡す必要があるデータへのポインター。
- callback - 新しいコールバックがキューに送信されるときに呼び出される関数。
- token - コールバックを削除するために、以降の XTaskQueueUnregisterMonitor の呼び出しで使用されるトークン。
たとえば、XTaskQueueRegisterMonitor の呼び出しは次のようになります。
XTaskQueueRegisterMonitor(queue, nullptr, HandleAsyncQueueCallback, &m_callbackToken);
対応する XTaskQueueMonitorCallback コールバックは次のように実装されます。
void CALLBACK HandleAsyncQueueCallback(
_In_opt_ void* context,
_In_ XTaskQueueHandle queue,
_In_ XTaskQueuePort port)
{
switch (port)
{
case XTaskQueuePort::Work:
{
std::lock_guard<std::mutex> lock(g_workReadyMutex);
g_workReady = true;
}
g_workReadyConditionVariable.notify_one(); // (std::condition_variable)
break;
}
}
その後、バックグラウンド スレッドで、この条件変数をリッスンし、XTaskQueueDispatch をウェイクアップして呼び出すことができます。
void BackgroundWorkThreadProc(XTaskQueueHandle queue)
{
while (true)
{
{
std::unique_lock<std::mutex> cvLock(g_workReadyMutex);
g_workReadyConditionVariable.wait(cvLock, [] { return g_workReady; });
if (g_stopBackgroundWork)
{
break;
}
g_workReady = false;
}
bool workFound = false;
do
{
workFound = XTaskQueueDispatch(queue, XTaskQueuePort::Work, 0);
} while (workFound);
}
XTaskQueueCloseHandle(queue);
}