다음을 통해 공유


XSAPI C API에서 비동기 호출

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

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

XSAPI C API는 XblSocialGetSocialRelationshipsAsync(), XblProfileGetUserProfileAsync(), XblAchievementsGetAchievementsForTitleIdAsync()와 같은 비동기 API 호출 시 개발자에게 직접적인 스레드 제어 권한을 부여하는 새로운 비동기 C 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);

이 호출 패턴을 이해하려면 XAsyncBlockXTaskQueueHandle 사용법을 잘 알아야 합니다.

  • XAsyncBlock비동기 작업완료 콜백과 관련된 모든 정보를 전달합니다.

  • XTaskQueueHandle을 통해 어떤 스레드에서 비동기 작업을 실행하고 어떤 스레드에서 XAsyncBlock의 완료 콜백을 호출하는지 확인할 수 있습니다.

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에 포함된 것은 다음과 같습니다.

  • - 작업을 실행할 위치에 관한 정보를 표시하는 핸들인 XTaskQueueHandle입니다. 이것이 설정되지 않은 경우 기본 큐가 사용됩니다.

  • 컨텍스트 - 데이터를 콜백 함수로 전달할 수 있습니다.

  • 콜백 - 비동기 작업이 완료된 후 호출될 선택적 콜백 함수입니다. 콜백을 지정하지 않는 경우 XAsyncBlockXAsyncGetStatus로 완료되기를 기다렸다가 결과를 얻을 수 있습니다.

호출하는 각 비동기 API의 힙에서 새로운 XAsyncBlock을 생성해야 합니다. XAsyncBlock은 XAsyncBlock의 완료 콜백이 호출된 후 삭제될 때까지 유지되어야 합니다.

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

비동기 작업 대기

비동기 작업이 완료되었는지 여부는 다양한 방법으로 알 수 있습니다.

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

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

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

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

결과를 얻기 위해 대부분의 비동기 API 함수에는 비동기 호출의 결과를 수신하는 해당 [함수 이름]결과 함수가 포함되어 있습니다.

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

결과 검색에 대한 자세한 내용은 각 비동기 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는 다음과 같은 매개 변수를 사용합니다.

  • - 콜백을 제출하려는 비동기 큐입니다.
  • 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);
}

참고 항목

XSAPI C API 소개

XSAPI 참조

libHttpClient