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();
}
}
この拡張メソッドではAsyncOperation
をAwaitable
型に変換しています。
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
メソッドが新設されているようです。
- Rendering.AsyncGPUReadback-RequestAsync - Unity スクリプトリファレンス
- AsyncAwaitTest/AsyncReadbackSample.cs at main · keijiro/AsyncAwaitTest · GitHub
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
まとめ
AsyncOperation
をAwaitable
に変換することでawait可能にするAsyncOperationAwaitableExtensions.GetAwaiter
メソッドの紹介と、その動作について解説しました。