AsyncOperationAwaitableExtensionsで、AsyncOperationをawaitableにする

keijiroさんのAsyncAwaitTestレポジトリをのぞいていると、下記のようにUnityWebRequestのHTTPリクエストなどの非同期処理がawait可能なことに気づきました。

AsyncAwaitTest/WebRequestSample.cs at main · keijiro/AsyncAwaitTest · GitHub

using UnityEngine;
using UnityWebRequest = UnityEngine.Networking.UnityWebRequest;

public sealed class WebRequestSample : MonoBehaviour
{
    [SerializeField] string _uri = "https://unity.com";

    async void Start()
    {
        using var req = UnityWebRequest.Get(_uri);
        await req.SendWebRequest();
        Debug.Log($"{req.result}: {req.downloadedBytes} bytes");
    }
}

標準でかなりスッキリとasync/awaitの処理がかけるようになったんだと思いつつ、これがどう実装されているんだろうと気になり、その動作原理(というほどたいそうなものではないけど)を調べてみました。

Unityの非同期処理とAsyncOperation

Unityで非同期処理を行うメソッドの多くはAsyncOperationか、それを継承した型の戻り値を返します。 処理は非同期で行われますが、この型のisDoneというプロパティを監視し続けることで、処理の終了タイミングがわかります。

たとえばコルーチンを用いてUnityWebRequestのリクエストを非同期で行うには、下記のように記述できます。

IEnumerator Start()
{
    using var request = UnityWebRequest.Get(_uri);
    AsyncOperation operation = request.SendWebRequest();
    while (!operation.isDone)
    {
        // 処理実行中
        yield return null;
    }

    // 処理完了
    Debug.Log($"{request.result}: {request.downloadedBytes} bytes");
}

ただし、AsyncOperationはawaitできません

AwaitableとAsyncOperationAwaitableExtensions

では、はじめに紹介したUnityWebRequestのawaitはどのように実現しているのかというと、下記のようなUnity 2023.1にはAsyncOperationAwaitableExtensions.GetAwaiterというAsyncOperationに対する拡張メソッドがUnityEngine名前空間内に実装されているためです。

namespace UnityEngine
{
    public static class AsyncOperationAwaitableExtensions
    {
        [ExcludeFromDocs]
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static Awaitable.Awaiter GetAwaiter(this AsyncOperation op)
             => Awaitable.FromAsyncOperation(op).GetAwaiter();
    }
}

この拡張メソッドではAsyncOperationAwaitable型に変換しています。 AwaitableはUnity 2023.1で実装された、メインスレッド上で非同期処理を行うためのAPIですが、この型がGetAwaiterメソッドを実装しているためawait可能です。(参考: 非同期メソッドの内部実装 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C

これにより、間接的に**AsyncOperationがawait可能になったということです**。

先述の通りUnityの非同期処理の多くはAsyncOperationか、それを基底にした型を返す(UnityWebRequestならUnityWebRequestAsyncOperationを、SceneManager.LoadSceneAsyncは直接AsyncOperationを返す)ので、標準APIの非同期処理の多くはawait可能になったといえます。

ちなみに余談ですが、GPU処理を非同期に実行するAsyncGPUReadbackは、下記のようにAwaitable<AsyncGPUReadbackRequest>を返却するRequestAsyncメソッドが新設されているようです。

async void Start()
{
    var frameCount = Time.frameCount;
    var req = await AsyncGPUReadback.RequestAsync(_texture, 0);

    Debug.Log(frameCount);
    Debug.Log(req.GetData<Color32>()[0]);
}

この実装から、今後実装されるUnityの非同期処理についてはAwaitableを直接返却するAPIも合わせて提供される可能性がありそうです。

AsyncOperationにCancellationTokenを渡す

AsyncOperationAwaitableExtensions経由でAsyncOperationを直感的にawaitできることがわかりましたが、注意点としてはCancellationTokenを渡すことができていない点です。

これは、AsyncOperationAwaitableExtensions.GetAwaiter(this AsyncOperation op)が内部で呼び出しているAwaitable.FromAsyncOperationが下記のように第2引数でCancellationTokenを受け取るため、こちらを直接呼び出すことで実現できます。

/// <summary>
///   <para>Creates an Awaitable from an existing AsyncOperation object.</para>
/// </summary>
/// <param name="op">Async operation object.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Awaitable FromAsyncOperation(
  AsyncOperation op,
  CancellationToken cancellationToken = default (CancellationToken))
{
  cancellationToken.ThrowIfCancellationRequested();
  return Awaitable.FromNativeAwaitableHandle(Awaitable.FromAsyncOperationInternal(op.m_Ptr), cancellationToken);
}

具体的には下記のように記述します。

async void Start()
{
    using var request = UnityWebRequest.Get("https://unity.com");
    // たとえばゲームオブジェクトのライフサイクルに紐付いたCancellationTokenを渡す
    await Awaitable.FromAsyncOperation(
        request.SendWebRequest(), destroyCancellationToken);
}

余談: UniTaskでのGetAwaiter

UniTaskでもUnityWebRequestを下記のようにawaitできます(参考: Basics of UniTask and AsyncOperation)。

var txt = (await UnityWebRequest.Get("https://...").SendWebRequest())
    .downloadHandler.text;

これもAwaitable APIと同様、下記のUnityAsyncExtensions.GetAwaiterという拡張メソッドが用意されています。これによりAsyncOperationからUniTaskへの変換が行われます。

UniTask/UnityAsyncExtensions.cs at master · Cysharp/UniTask · GitHub

namespace Cysharp.Threading.Tasks
{
    public static partial class UnityAsyncExtensions
    {
#if !UNITY_2023_1_OR_NEWER
        // from Unity2023.1.0a15, AsyncOperationAwaitableExtensions.GetAwaiter is defined in UnityEngine.

        public static AsyncOperationAwaiter GetAwaiter(this AsyncOperation asyncOperation)
        {
            Error.ThrowArgumentNullException(asyncOperation, nameof(asyncOperation));
            return new AsyncOperationAwaiter(asyncOperation);
        }
#endif

まとめ

AsyncOperationAwaitableに変換することでawait可能にするAsyncOperationAwaitableExtensions.GetAwaiterメソッドの紹介と、その動作について解説しました。