Share via


PlayFab 서비스 SDK에서 비동기 호출 만들기

비동기 API는 비동기 작업을 신속하게 반환하고 시작하는 API로서, 작업이 완료되면 결과가 반환됩니다.

일반적으로 게임은 완료 콜백 사용 시 어떤 스레드에서 비동기 작업을 실행하고 어떤 스레드에서 결과를 반환할지 제어할 수 있는 권한이 거의 없었습니다. 일부 게임은 스레드 동기화가 필요 없도록 힙 섹션에 대한 작업을 단일 스레드만 할 수 있게 설계되었습니다. 완료 콜백이 게임 제어 스레드에서 호출되지 않는 경우 비동기 작업의 결과로 공유 상태를 업데이트하려면 스레드 동기화가 필요합니다.

PlayFab 서비스 SDK는 PFAuthenticationLoginWithCustomIDAsync, PFDataGetFilesAsync 또는 PFProfilesGetProfileAsync와 같은 비동기 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에 포함된 것은 다음과 같습니다.

  • queue - 작업을 실행할 위치에 대한 정보를 나타내는 핸들인 XTaskQueueHandle입니다. 이 매개 변수를 설정하지 않으면 기본 큐가 사용됩니다.
  • 컨텍스트 - 데이터를 콜백 함수로 전달할 수 있습니다.
  • 콜백 - 비동기 작업이 완료된 후 호출될 선택적 콜백 함수입니다. 콜백을 지정하지 않으면 XAsyncBlockXAsyncGetStatus와 함께 완료될 때까지 기다린 다음 결과를 얻을 수 있습니다.

수행하는 각 비동기 호출에 대해 힙에 새 XAsyncBlock을 생성해야 합니다. XAsyncBlockXAsyncBlock의 완료 콜백이 호출된 후 삭제할 수 있을 때까지 유지되어야 합니다.

중요:

XAsyncBlock은 비동기 작업이 완료될 때까지 메모리에 유지되어야 합니다. 동적으로 할당된 경우 XAsyncBlock의 완료 콜백 내에서 삭제할 수 있습니다.

비동기 작업 대기

다음 두 가지 방법으로 비동기 작업이 완료되었음을 알 수 있습니다.

  • XAsyncBlock의 완료 콜백이 호출됩니다.
  • XAsyncGetStatus를 true로 호출하고 완료될 때까지 기다립니다.

XAsyncGetStatus를 사용하면 XAsyncBlock의 완료 콜백이 실행된 후 비동기 작업이 완료된 것으로 간주되지만 XAsyncBlock의 완료 콜백은 선택 사항입니다.

비동기 작업이 완료되면 결과를 얻을 수 있습니다.

비동기 작업의 결과 가져오기

결과를 얻기 위해 대부분의 비동기 API 함수에는 비동기 호출의 결과를 수신하는 해당 Result 함수가 있습니다.

예제 코드에서 PFProfilesGetProfileAsync에는 해당하는 PFProfilesGetProfileGetResult 함수가 있습니다. 이 함수를 사용해 함수의 결과를 검색하고 그에 따라 조치를 취할 수 있습니다.

결과 검색에 대한 자세한 내용은 각 비동기 API 함수에 관한 설명서를 참조하세요.

XTaskQueueHandle

XTaskQueueHandle을 사용하면 비동기 작업을 실행하는 스레드와 XAsyncBlock의 완료 콜백을 호출하는 스레드를 결정할 수 있습니다.

디스패치 모드를 설정하여 이러한 작업을 수행하는 스레드를 제어할 수 있습니다. 다음과 같이 세 가지 디스패치 모드를 사용할 수 있습니다.

  • 수동 - 수동 큐는 자동으로 발송되지 않습니다. 원하는 스레드에서 발송하는 것은 개발자 책임입니다. 이것을 사용해 비동기 호출의 작업 또는 콜백 측면을 특정 스레드에 할당할 수 있습니다.
  • 스레드 풀 - 스레드 풀을 사용해 디스패치합니다. 스레드 풀은 호출을 병렬로 호출하며 스레드 풀 스레드를 사용할 수 있게 되면 큐에서 차례로 호출을 실행합니다. 스레드 풀은 사용하기 가장 쉽지만 사용되는 스레드에 대한 최소한의 제어 권한을 제공합니다.
  • 직렬화된 스레드 풀 - 스레드 풀을 사용해 디스패치합니다. 스레드 풀은 단일 스레드 풀 스레드가 사용 가능해지면 차례로 큐에서 호출을 실행하여 순차적으로 호출을 호출합니다.
  • 즉시 - 제출이 이루어진 스레드에서 대기 중인 작업을 즉시 디스패치합니다.

XTaskQueueHandle을 생성하려면 XTaskQueueCreate를 호출해야 합니다. 예를 들면 :

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

이 함수는 두 개의 XTaskQueueDispatchMode 매개 변수를 사용합니다. 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는 비동기 작업을 처리하는 스레드의 디스패치 모드를 결정합니다. 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);
  • - 작업을 디스패치할 큐입니다.
  • 포트 - XTaskQueuePort 열거형의 인스턴스입니다.
  • timeoutInMs - 시간 제한에 대한 uint32_t(밀리초)입니다.

다음과 같이 XTaskQueuePort 열거형에서 정의하는 두 가지 콜백 유형이 있습니다.

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

참조

API 참조 설명서.