PlayFab Services SDK で非同期呼び出しを行う

非同期 API は、すばやくを制御を返し、非同期タスクを開始する API で、タスクが完了すると結果が返されます。

従来のゲームでは、完了コールバックを使用する場合に、どのスレッドが非同期タスクを実行し、どのスレッドが結果を返すかをほとんど制御していません。 ゲームによっては、スレッドの同期の必要性を回避するために、ヒープのセクションを 1 つのスレッドでのみ操作できるように設計されているものもあります。 完了コールバックが、ゲームが制御するスレッドから呼び出されていない場合、非同期タスクの結果で共有状態を更新するには、スレッドの同期が必要になります。

PlayFab Services SDK は、PFAuthenticationLoginWithCustomIDAsyncPFDataGetFilesAsyncPFProfilesGetProfileAsync など、非同期 API 呼び出しを行うときに直接スレッド制御を開発者に提供する非同期 C API を公開します。

PFProfilesGetProfileAsync を呼び出す基本的な例を次に示します。

    XAsyncBlock* asyncBlock = new XAsyncBlock();
    asyncBlock->queue = GlobalState()->queue;
    asyncBlock->context = nullptr;
    asyncBlock->callback = [](XAsyncBlock* asyncBlock)
    {
        std::unique_ptr<XAsyncBlock> asyncBlockPtr{ asyncBlock }; // take ownership of XAsyncBlock
        
        size_t bufferSize;
        HRESULT hr = PFProfilesGetProfileGetResultSize(asyncBlock, &bufferSize);
        if (SUCCEEDED(hr))
        {
            std::vector<char> getProfileResultBuffer(bufferSize);
            PFProfilesGetEntityProfileResponse* getProfileResponseResult{ nullptr };
            PFProfilesGetProfileGetResult(asyncBlock, getProfileResultBuffer.size(), getProfileResultBuffer.data(), &getProfileResponseResult, nullptr);
        }
    };

    PFProfilesGetEntityProfileRequest profileRequest{};
    HRESULT hr = PFProfilesGetProfileAsync(GlobalState()->entityHandle, &profileRequest, asyncBlock);

この呼び出しのパターンを理解するには、XAsyncBlockXTaskQueueHandle の使い方を理解する必要があります。

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 には、次の要素が含まれます。

  • キュー - 1 つの作業を実行する場所に関する情報を表すハンドルである XTaskQueueHandle。 このパラメーターが設定されていない場合は、既定値のキューが使用されます。
  • context - データをコールバック関数に渡すことができるようにします。
  • callback - 非同期処理が完了した後に呼び出される、オプションのコールバック関数。 コールバックを指定しない場合、XAsyncGetStatus を使用して XAsyncBlock が完了するまで待機し、結果を取得します。

非同期呼び出しごとに、ヒープで新しい XAsyncBlock を作成する必要があります。 XAsyncBlock は、XAsyncBlock の完了コールバックが呼び出されるまで存在している必要があります。その後、XAsyncBlock を削除できます。

重要:

XAsyncBlock は、非同期タスクが完了するまでメモリ内に存在している必要があります。 動的に割り当てられる場合は、XAsyncBlock の完了コールバック内で削除できます。

非同期タスクの待機

非同期タスクが完了したことは、次の 2 つの方法で知ることができます。

  • XAsyncBlock の完了コールバックが呼び出される。
  • XAsyncGetStatus を呼び出して、true の場合は非同期タスクが完了するまで待機する。

XAsyncGetStatus では、非同期タスクは、XAsyncBlock の完了コールバックが実行された後、完了したと見なされますが、XAsyncBlock の完了コールバックはオプションになります。

非同期タスクが完了したら、結果を取得できます。

非同期タスクの結果の取得

結果を取得するために、ほとんどの非同期 API 関数には、非同期呼び出しの結果を受け取るための対応する Result 関数があります。

このコード例では、 PFProfilesGetProfileAsync に対応する PFProfilesGetProfileGetResult 関数があります。 この関数を使用して、関数の結果を取得し、それに応じて処理を実行できます。

結果の取得について詳しくは、それぞれの非同期 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 は次のパラメーターを受け取ります。

  • キュー - コールバックを送信している対象の非同期キュー。
  • callbackContext - 送信コールバックに渡す必要があるデータへのポインター。
  • コールバック - 新しいコールバックがキューに送信されるときに呼び出される関数。
  • トークン - コールバックを削除するために、以降の 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);
}

リファレンス

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