async/awaitがわからない
Unity開発では、日頃からTaskやUniTaskを使う機会が多く、便利に使わせてもらっていますが、async/awaitが何なのかよく理解しないまま雰囲気で使っていたので、async/awaitを使ったAwaitableパターンというものを独自実装をして検証してみました。
調べているうちに、こちらの大変参考になりそうな記事を見つけたのですが、自分にはまだ早すぎて理解が追いつけなかったので、簡単なテストコードの理解から始めることにしましたw
テストコード
Awaitableパターンの独自実装は、こちらのサイトを参考にしました。
どうやら、Awaitableパターンとは、特定のインターフェースや、メソッドを実装したクラスを、コンパイラが認識すると、async/await演算子を使えるようになる仕組みのようです。
試しに、現在時刻を非同期で取得して表示するテストコードを書いてみました。
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using UnityEngine;
namespace AwaitTest
{
// テスト用のMonoBehaviour
public class MyAwaiterTest : MonoBehaviour
{
private async void Start()
{
Log("Start");
var result = await MyAwaitable<DateTime>.StartMyWork(() =>
{
Log("Working");
Thread.Sleep(5000);
return DateTime.Now;
});
Log("End");
Log($"Result: {result}");
}
public static void Log(string str)
{
Debug.Log($"[{Thread.CurrentThread.ManagedThreadId}] {str}");
}
}
public class MyAwaitable<T>
{
private MyAwaitable() {}
// 完了フラグ
public bool MyIsCompleted { get; private set; }
// 実行結果を保持する。
public T MyResult { get; private set; }
// 処理完了後に呼ぶデリゲート
private Action MyOnCompleted { get; set; }
// Awaitableに必須のメソッド。名前は変えられない。
public MyAwaiter<T> GetAwaiter()
{
MyAwaiterTest.Log($"GetAwaiter");
return new MyAwaiter<T>(this);
}
private void DoMyWork(Func<T> action)
{
MyAwaiterTest.Log($"DoMyWork");
ThreadPool.QueueUserWorkItem(_ =>
{
MyAwaiterTest.Log($"ExecuteQueue: Start");
MyResult = action();
MyIsCompleted = true;
MyOnCompleted?.Invoke();
MyAwaiterTest.Log($"ExecuteQueue: End");
});
}
// 完了時に呼び出される処理の登録。
public void MyContinueWith(Action action)
{
MyAwaiterTest.Log($"MyContinueWith: SetAction");
MyOnCompleted = action;
if (MyIsCompleted && MyOnCompleted != null)
{
// すでに完了している場合は即時呼び出し
MyAwaiterTest.Log($"MyContinueWith: ExecuteAction");
MyOnCompleted();
}
}
// 非同期処理を開始する。
public static MyAwaitable<T> StartMyWork(Func<T> action)
{
MyAwaiterTest.Log($"StartMyWork");
var awaitable = new MyAwaitable<T>();
awaitable.DoMyWork(action);
return awaitable;
}
}
public class MyAwaiter<T> : INotifyCompletion
{
private readonly MyAwaitable<T> _myTarget;
public MyAwaiter(MyAwaitable<T> myTarget)
{
_myTarget = myTarget;
}
// Awaiterに必須のプロパティ。処理が完了しているかどうかを返す。名前は変えられない。
public bool IsCompleted
{
get
{
MyAwaiterTest.Log($"IsCompleted: {_myTarget.MyIsCompleted}");
return _myTarget.MyIsCompleted;
}
}
// Awaiterに必須のメソッド。処理の結果を返す。名前は変えられない。
public T GetResult()
{
MyAwaiterTest.Log($"GetResult");
return _myTarget.MyResult;
}
// Awaiterに必須のメソッド。INotifyCompletionの実装。
// 渡されたデリゲートを、対象となる非同期処理の完了時にコールバックされるように登録する。
public void OnCompleted(Action continuation)
{
MyAwaiterTest.Log($"OnCompleted");
// 実行スレッドの同期コンテキスト上で継続処理を実行
var context = SynchronizationContext.Current;
_myTarget.MyContinueWith(() =>
{
MyAwaiterTest.Log($"MyContinueWith: Start");
context.Post(_ =>
{
MyAwaiterTest.Log($"MyContinueWith: Post");
continuation();
}, null);
});
}
}
}
My~
というメソッドやプロパティは、自由に変更可能な名前です。GetAwaiter
、IsCompleted
、GetResult
などは、インターフェースで定義はされていませんが、名前を変えると、Awaitableパターンと認識されずにコンパイルエラーになります。なんだか違和感がありますが、参考サイトで触れられているようにダック・タイピング的な制約になっているようです。
実行結果
適当なGameObjectにMyAwaiterTestをアタッチして、Unityエディタから実行すると、このようなログが取得できました。緑背景のログは、別スレッドのログです。
別スレッドに処理を逃しつつ、裏で後続処理の登録や、処理の完了確認を行っているようでした。ログのスタックトレースを確認してみると、System.Runtime.CompilerServices.AsyncVoidMethodBuilder
というクラスがAwaiterの取得を行ったり、IsCompletedフラグを確認していました。
ログを取得することによって、Awaitableパターンの処理の流れを大雑把に把握することができました。次の疑問は、AsyncVoidMethodBuilderとは一体何者なのか…
<日記はここで終わっている>
検証環境
Unity: 2021.3.8f1
OS: Windows10
Discussion