Open3
Unreal Engine C++ の Async についてまとめる
世の中の Unreal Engine C++ の非同期処理系の記事は若干古いものも多く、勘違いしていたものも多かったため、自分のメモとしてまとめていく
Async関数
/**
* Execute a given function asynchronously.
*
* Usage examples:
*
* // using global function
* int TestFunc()
* {
* return 123;
* }
*
* TUniqueFunction<int()> Task = TestFunc();
* auto Result = Async(EAsyncExecution::Thread, Task);
*
* // using lambda
* TUniqueFunction<int()> Task = []()
* {
* return 123;
* }
*
* auto Result = Async(EAsyncExecution::Thread, Task);
*
*
* // using inline lambda
* auto Result = Async(EAsyncExecution::Thread, []() {
* return 123;
* }
*
* @param CallableType The type of callable object.
* @param Execution The execution method to use, i.e. on Task Graph or in a separate thread.
* @param Function The function to execute.
* @param CompletionCallback An optional callback function that is executed when the function completed execution.
* @return A TFuture object that will receive the return value from the function.
*/
template<typename CallableType>
auto Async(EAsyncExecution Execution, CallableType&& Callable, TUniqueFunction<void()> CompletionCallback = nullptr) -> TFuture<decltype(Forward<CallableType>(Callable)())>
タスクをブロックさせたくない処理がある場合、ConstructAndDispatchWhenReady()
を用いたり、FAsyncTask
クラスを使ったりする記事が多いが、今はこの関数を使っておけばよさそう。
上記の例には EAsyncExecution::Thread
しかないが、以下のようにいくつか種類がある。
/**
* Enumerates available asynchronous execution methods.
*/
enum class EAsyncExecution
{
/** Execute in Task Graph (for short running tasks). */
TaskGraph,
/** Execute in Task Graph on the main thread (for short running tasks). */
TaskGraphMainThread,
/** Execute in separate thread if supported (for long running tasks). */
Thread,
/** Execute in separate thread if supported or supported post fork (see FForkProcessHelper::CreateThreadIfForkSafe) (for long running tasks). */
ThreadIfForkSafe,
/** Execute in global queued thread pool. */
ThreadPool,
#if WITH_EDITOR
/** Execute in large global queued thread pool. */
LargeThreadPool
#endif
};
新たに Thread を作成し実行することもできるし、MainThread で実行することも可能。(内部実装を見ればわかるが、MainThread を指定すると、ConstructAndDispatchWhenReady()
を GameThread で実行するようになっている。
この関数は TFuture
を返す。これを使って後続の処理を手続き的に記述することが可能。
TPromise/TFutureクラス
以下の記事や C++ の std::promise/std::future の機能から、タスクの処理をブロックし、結果を待つ用途でしか使用できないと思っていた。
実は、TFuture
には Then()
や Next()
といったメソッドが実装されており、以下ような実装をすると、呼び出し元のスレッド(例えば GameThread など)をブロックすることなく実行することが可能。
void TestFunction()
{
TFuture<int32> Res = Async(EAsyncExecution::TaskGraphMainThread, []() {
UE_LOG(LogTemp, Log, TEXT("Executing async task."));
return 33;
});
Res.Next([](int32 Value) {
UE_LOG(LogTemp, Log, TEXT("Async task completed. Result: %d"), Value);
});
UE_LOG(LogTemp, Log, TEXT("Exiting function."));
}
// LogTemp: Exiting function.
// LogTemp: Executing async task.
// LogTemp: Async task completed. Result: 33
Then()
は TFuture
を受け取り、Next()
は TFuture
を無効にして結果を受け取ることができる。
メソッドチェーンも可能。
void TestFunction()
{
TFuture<int32> Res = Async(EAsyncExecution::TaskGraphMainThread, []() {
UE_LOG(LogTemp, Log, TEXT("Executing async task."));
return 33;
});
Res.Next([](int32 Value) {
UE_LOG(LogTemp, Log, TEXT("First async task completed. Result: %d"), Value);
return 44;
}).Next([](int32 Value) {
UE_LOG(LogTemp, Log, TEXT("Second async task completed. Result: %d"), Value);
});
UE_LOG(LogTemp, Log, TEXT("Exiting function."));
}
// LogTemp: Exiting function.
// LogTemp: Executing async task.
// LogTemp: First async task completed.Result: 33
// LogTemp: Second async task completed.Result: 44
よくある HTTP リクエスト処理は以下のようになる。(デリゲートをたらい回ししなくてよくなる)
TFuture<TOptional<FString>> RequestWithTFuture(const FString& Url, const FString& Verb) {
auto Promise = MakeShared<TPromise<TOptional<FString>>>();;
TFuture<TOptional<FString>> FutureResult = Promise->GetFuture();
const auto Req = FHttpModule::Get().CreateRequest();
Req->OnProcessRequestComplete().BindLambda([CapturedPromise = MoveTemp(Promise)](FHttpRequestPtr Req, FHttpResponsePtr Res, bool Suc) mutable {
if (Suc && EHttpResponseCodes::IsOk(Res->GetResponseCode())) {
CapturedPromise->SetValue(Res->GetContentAsString());
}
else {
CapturedPromise->SetValue(NullOpt);
}
});
Req->SetURL(Url);
Req->SetVerb(Verb);
Req->ProcessRequest();
return FutureResult;
}
void TestFunction()
{
RequestWithTFuture("https://jsonplaceholder.typicode.com/todos/1", "GET").Next([](TOptional<FString> Value) {
UE_LOG(LogTemp, Log, TEXT("Result: %s"), *(Value.Get("Request failed: Something went wrong.")));
});
}