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);
이 호출 패턴을 이해하려면 XAsyncBlock 및 XTaskQueueHandle 사용법을 잘 알아야 합니다.
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입니다. 이것이 설정되지 않은 경우 기본 큐가 사용됩니다.
컨텍스트 - 데이터를 콜백 함수로 전달할 수 있습니다.
콜백 - 비동기 작업이 완료된 후 호출될 선택적 콜백 함수입니다. 콜백을 지정하지 않는 경우 XAsyncBlock이 XAsyncGetStatus로 완료되기를 기다렸다가 결과를 얻을 수 있습니다.
호출하는 각 비동기 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);
}