Open3

Unreal Engine C++ の Async についてまとめる

hakoaihakoai

世の中の Unreal Engine C++ の非同期処理系の記事は若干古いものも多く、勘違いしていたものも多かったため、自分のメモとしてまとめていく

hakoaihakoai

Async関数

https://github.com/EpicGames/UnrealEngine/blob/release/Engine/Source/Runtime/Core/Public/Async/Async.h

/**
 * 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 を返す。これを使って後続の処理を手続き的に記述することが可能。

hakoaihakoai

TPromise/TFutureクラス

以下の記事や C++ の std::promise/std::future の機能から、タスクの処理をブロックし、結果を待つ用途でしか使用できないと思っていた。
https://usagi.hatenablog.jp/entry/2017/06/10/122720

実は、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.")));
    });
}