Ausführen asynchroner Aufrufe im PlayFab Services SDK

Eine asynchrone API ist eine API, die schnell zurückgibt, aber eine asynchrone Aufgabe startet, und das Ergebnis wird zurückgegeben, nachdem die Aufgabe abgeschlossen wurde.

Bisher hatten Spiele wenig Kontrolle darüber, welcher Thread die asynchrone Aufgabe ausführt und welcher Thread die Ergebnisse zurückgibt, wenn ein Abschlussrückruf verwendet wird. Einige Spiele sind so konzipiert, dass ein Abschnitt des Heaps nur von einem einzelnen Thread berührt wird, um eine Threadsynchronisierung zu vermeiden. Wenn der Abschlussrückruf nicht von einem Thread aufgerufen wird, den das Spiel steuert, erfordert das Aktualisieren des freigegebenen Zustands mit dem Ergebnis einer asynchronen Aufgabe eine Threadsynchronisierung.

Das PlayFab Services SDK macht eine asynchrone C-API verfügbar, die Entwicklern direkte Threadsteuerung bietet, wenn sie einen asynchronen API-Aufruf ausführen, z. B. PFAuthenticationLoginWithCustomIDAsync, PFDataGetFilesAsync oder PFProfilesGetProfileAsync.

Hier ist ein einfaches Beispiel zum Aufrufen von 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);

Um dieses Aufrufmuster zu verstehen, müssen Sie verstehen, wie XAsyncBlock und XTaskQueueHandle verwendet werden.

Der XAsyncBlock enthält alle Informationen zum asynchronen Task- und Abschlussrückruf.

Mit XTaskQueueHandle können Sie bestimmen, welcher Thread die asynchrone Aufgabe ausführt und welcher Thread den Abschlussrückruf des XAsyncBlock aufruft.

Der XAsyncBlock

Sehen wir uns den XAsyncBlock im Detail an. Es handelt sich um eine Struktur, die wie folgt definiert ist:

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];
};

Der XAsyncBlock enthält Folgendes:

  • queue : ein XTaskQueueHandle , ein Handle, der Informationen darüber darstellt, wo ein Teil der Arbeit ausgeführt werden soll. Wenn dieser Parameter nicht festgelegt ist, wird eine Standardwarteschlange verwendet.
  • context : Ermöglicht das Übergeben von Daten an die Rückruffunktion.
  • callback : Eine optionale Rückruffunktion, die aufgerufen wird, nachdem die asynchrone Arbeit abgeschlossen wurde. Wenn Sie keinen Rückruf angeben, können Sie warten, bis der XAsyncBlock mit XAsyncGetStatus abgeschlossen ist, und dann die Ergebnisse abrufen.

Sie sollten für jeden asynchronen Aufruf, den Sie tätigen, einen neuen XAsyncBlock auf dem Heap erstellen. Der XAsyncBlock muss bis zum Aufruf des Abschlussrückrufs des XAsyncBlocks aktiv sein und kann dann gelöscht werden.

Wichtig:

Ein XAsyncBlock muss im Arbeitsspeicher verbleiben, bis die asynchrone Aufgabe abgeschlossen ist. Wenn es dynamisch zugeordnet wird, kann es innerhalb des Abschlussrückrufs des XAsyncBlocks gelöscht werden.

Warten auf eine asynchrone Aufgabe

Sie können feststellen, dass eine asynchrone Aufgabe auf zwei verschiedene Arten abgeschlossen ist:

  • Der Abschlussrückruf des XAsyncBlock wird aufgerufen.
  • Rufen Sie XAsyncGetStatus mit true auf, um zu warten, bis der Vorgang abgeschlossen ist.

Bei XAsyncGetStatus gilt die asynchrone Aufgabe als abgeschlossen, nachdem der Abschlussrückruf des XAsyncBlock ausgeführt wurde. Der Abschlussrückruf des XAsyncBlock ist jedoch optional.

Sobald die asynchrone Aufgabe abgeschlossen ist, können Sie die Ergebnisse abrufen.

Abrufen des Ergebnisses der asynchronen Aufgabe

Zum Abrufen des Ergebnisses verfügen die meisten asynchronen API-Funktionen über eine entsprechende Result-Funktion, um das Ergebnis des asynchronen Aufrufs zu empfangen.

In unserem Beispielcode verfügt PFProfilesGetProfileAsync über eine entsprechende PFProfilesGetProfileGetResult-Funktion . Sie können diese Funktion verwenden, um das Ergebnis der Funktion abzurufen und entsprechend zu handeln.

Ausführliche Informationen zum Abrufen von Ergebnissen finden Sie in der Dokumentation der einzelnen asynchronen API-Funktionen.

Das XTaskQueueHandle

Mit XTaskQueueHandle können Sie bestimmen, welcher Thread die asynchrone Aufgabe ausführt und welcher Thread den Abschlussrückruf des XAsyncBlock aufruft.

Sie können steuern, welcher Thread diese Vorgänge ausführt, indem Sie einen Dispatchmodus festlegen. Es stehen drei Dispatchmodi zur Verfügung:

  • Manuell : Die manuelle Warteschlange wird nicht automatisch verteilt. Es liegt an dem Entwickler, sie in einem beliebigen Thread zu verteilen. Dies kann verwendet werden, um entweder die Arbeits- oder Rückrufseite eines asynchronen Aufrufs einem bestimmten Thread zuzuweisen.
  • Threadpool : Verteilt mithilfe eines Threadpools. Der Threadpool ruft die Aufrufe parallel auf und führt einen Aufruf aus der Warteschlange aus, wenn Threadpoolthreads verfügbar werden. Der Threadpool ist am einfachsten zu verwenden, bietet Ihnen jedoch die geringste Kontrolle darüber, welcher Thread verwendet wird.
  • Serialisierter Threadpool : Verteilt mithilfe eines Threadpools. Der Threadpool ruft die Aufrufe seriell auf und führt einen Aufruf aus der Warteschlange aus, wenn der Thread für den Einzelthreadpool verfügbar wird.
  • Sofort : Sendet sofort die Arbeit in der Warteschlange für den Thread, von dem sie übermittelt wurde.

Um ein neues XTaskQueueHandle zu erstellen, müssen Sie XTaskQueueCreate aufrufen. Beispiel:

STDAPI XTaskQueueCreate(
    _In_ XTaskQueueDispatchMode workDispatchMode,
    _In_ XTaskQueueDispatchMode completionDispatchMode,
    _Out_ XTaskQueueHandle* queue
    ) noexcept;

Diese Funktion akzeptiert zwei XTaskQueueDispatchMode-Parameter . Es gibt drei mögliche Werte für XTaskQueueDispatchMode:

/// <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 bestimmt den Dispatchmodus für den Thread, der die asynchrone Arbeit verarbeitet. completionDispatchMode bestimmt den Dispatchmodus für den Thread, der den Abschluss des asynchronen Vorgangs verarbeitet.

Nachdem Sie Ihr XTaskQueueHandle erstellt haben, fügen Sie es einfach dem XAsyncBlock hinzu, um das Threading für Ihre Arbeits- und Vervollständigungsfunktionen zu steuern. Wenn Sie die Verwendung von XTaskQueueHandle abgeschlossen haben, können Sie es in der Regel mit XTaskQueueCloseHandle schließen:

STDAPI_(void) XTaskQueueCloseHandle(
    _In_ XTaskQueueHandle queue
    ) noexcept;

Anrufbeispiel:

XTaskQueueCloseHandle(queue);

Manuelles Senden eines XTaskQueueHandle-Elements

Wenn Sie den manuellen Warteschlangenverteilungsmodus für eine XTaskQueueHandle-Arbeits - oder Vervollständigungswarteschlange verwendet haben, müssen Sie die Verteilung manuell durchführen. Angenommen, es wurde ein XTaskQueueHandle erstellt, bei dem sowohl die Arbeitswarteschlange als auch die Vervollständigungswarteschlange wie folgt manuell verteilt werden:

XTaskQueueHandle queue = nullptr;
HRESULT hr = XTaskQueueCreate(
    XTaskQueueDispatchMode::Manual,
    XTaskQueueDispatchMode::Manual,
    &queue);

Um Arbeit zu verteilen, der XTaskQueueDispatchMode::Manual zugewiesen wurde, rufen Sie die XTaskQueueDispatch-Funktion auf.

STDAPI_(bool) XTaskQueueDispatch(
    _In_ XTaskQueueHandle queue,
    _In_ XTaskQueuePort port,
    _In_ uint32_t timeoutInMs
    ) noexcept;

Anrufbeispiel:

HRESULT hr = XTaskQueueDispatch(queue, XTaskQueuePort::Completion, 0);
  • queue : Die Warteschlange, an die die Arbeit verteilt werden soll.
  • port : eine instance der XTaskQueuePort-Enumeration.
  • timeoutInMs : ein uint32_t für das Timeout in Millisekunden.

Es gibt zwei Rückruftypen, die von der XTaskQueuePort-Enumeration definiert werden:

/// <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
};

Wann sollte XTaskQueueDispatch aufgerufen werden?

Um zu überprüfen, wann die Warteschlange ein neues Element empfangen hat, können Sie XTaskQueueRegisterMonitor aufrufen, um einen Ereignishandler festzulegen, um Ihren Code darüber zu informieren, dass entweder Arbeits- oder Vervollständigungen für die Verteilung bereit sind.

STDAPI XTaskQueueRegisterMonitor(
    _In_ XTaskQueueHandle queue,
    _In_opt_ void* callbackContext,
    _In_ XTaskQueueMonitorCallback* callback,
    _Out_ XTaskQueueRegistrationToken* token
    ) noexcept;

XTaskQueueRegisterMonitor akzeptiert die folgenden Parameter:

  • queue : Die asynchrone Warteschlange, für die Sie den Rückruf übermitteln.
  • callbackContext : Ein Zeiger auf Daten, die an den Submit-Rückruf übergeben werden sollen.
  • callback : Die Funktion, die aufgerufen wird, wenn ein neuer Rückruf an die Warteschlange übermittelt wird.
  • token : Ein Token, das in einem späteren Aufruf von XTaskQueueUnregisterMonitor verwendet wird, um den Rückruf zu entfernen.

Hier ist beispielsweise ein Aufruf von XTaskQueueRegisterMonitor:

XTaskQueueRegisterMonitor(queue, nullptr, HandleAsyncQueueCallback, &m_callbackToken);

Der entsprechende XTaskQueueMonitorCallback-Rückruf kann wie folgt implementiert werden:

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;
    }
}

Anschließend können Sie in einem Hintergrundthread lauschen, um diese Bedingungsvariable zu reaktivieren und XTaskQueueDispatch aufzurufen.

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);
}

Referenzen

API-Referenzdokumentation