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);
이 호출 패턴을 이해하려면 XAsyncBlock 및 XTaskQueueHandle 사용 방법을 이해해야 합니다.
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입니다. 이 매개 변수를 설정하지 않으면 기본 큐가 사용됩니다.
- 컨텍스트 - 데이터를 콜백 함수로 전달할 수 있습니다.
- 콜백 - 비동기 작업이 완료된 후 호출될 선택적 콜백 함수입니다. 콜백을 지정하지 않으면 XAsyncBlock이 XAsyncGetStatus와 함께 완료될 때까지 기다린 다음 결과를 얻을 수 있습니다.
수행하는 각 비동기 호출에 대해 힙에 새 XAsyncBlock을 생성해야 합니다. XAsyncBlock은 XAsyncBlock의 완료 콜백이 호출된 후 삭제할 수 있을 때까지 유지되어야 합니다.
중요:
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);
}