Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Deep Dive async/await in Unity with UniTask(UniRx.Async)

Yoshifumi Kawai
September 15, 2018
180

Deep Dive async/await in Unity with UniTask(UniRx.Async)

Yoshifumi Kawai

September 15, 2018
Tweet

More Decks by Yoshifumi Kawai

Transcript

  1. Reactive Extensions for Unity https://github.com/neuecc/UniRx/ async/await(UniTask) async UniTask<string> DemoAsync() {

    // You can await Unity's AsyncObject var asset = await Resources.LoadAsync<TextAsset>("foo"); // .ConfigureAwait accepts progress callback await SceneManager.LoadSceneAsync("scene2").ConfigureAwai // await frame-based operation(you can also await frame c await UniTask.Delay(TimeSpan.FromSeconds(3)); // like 'yield return WaitForEndOfFrame', or Rx's Observe await UniTask.Yield(PlayerLoopTiming.PostLateUpdate); // You can await standard task await Task.Run(() => 100); // get async webrequest async UniTask<string> GetTextAsync(UnityWebRequest req) { var op = await req.SendWebRequest(); return op.downloadHandler.text; } var task1 = GetTextAsync(UnityWebRequest.Get("http://goog var task2 = GetTextAsync(UnityWebRequest.Get("http://bing var task3 = GetTextAsync(UnityWebRequest.Get("http://yaho // concurrent async-wait and get result easily by tuple s var (google, bing, yahoo) = await UniTask.WhenAll(task1, // You can handle timeout easily await GetTextAsync(UnityWebRequest.Get("http://unity.com"
  2. 非同期を同期的に扱う仕組み static string GetSync(int page) { try { var url

    = "http://...?page=" + page; var html = GetHttpStringSync(url); return html; } catch { return "Error"; } } static async Task<string> GetAsync(int page) { try { var url = "http://...?page=" + page; var html = await GetHttpStringAsync(url); return html; } catch { return "Error"; } }
  3. Synchronous Asynchronous Single(1) Multiple(*) var x = f(); var x

    = await f(); var query = from person in sequence where person.Age >= 20 select person.Name; foreach (var item in query) { OnNext(item); } var query = from person in sequence where person.Age >= 20 select person.Name; query.Subscribe(item => { OnNext(item); }); IEnumerble<T> IObservable<T> T Task<T>
  4. Absoutely No C# 5.0(.NET 4.5)でasync/awaitを実装するにあたり、 既にある構造(.NET 4.0 Task)を流用するのが早かった そもそも広く使われるまで分からないこともある 結果、負債もある

    midori(MicrosoftのC#風言語によるマネージドOSプロジェクト)での 非同期モデルでの考察においても、Taskに関しては特にパフォーマ ンス面で(OSみたいなシビアなものを作るには)後悔されている http://joeduffyblog.com/2015/11/19/asynchronous-everything/ 現在のC#は、それをValueTaskの独自進化で返済していってる
  5. Absoutely No C# 5.0(.NET 4.5)でasync/awaitを実装するにあたり、 既にある構造(.NET 4.0 Task)を流用するのが早かった そもそも広く使われるまで分からないこともある 結果、負債もある

    midori(MicrosoftのC#風言語によるマネージドOSプロジェクト)での 非同期モデルでの考察においても、Taskに関しては特にパフォーマ ンス面で(OSみたいなシビアなものを作るには)後悔されている http://joeduffyblog.com/2015/11/19/asynchronous-everything/ 現在のC#は、それをValueTaskの独自進化で返済していってる
  6. async Task<string> SampleTextLoadAsync() { Debug.Log("Before LoadAsync:" +Time.frameCount); // frame:1 var

    textAsset = await Resources.LoadAsync<TextAsset>("te") as TextAsset; Debug.Log("After LoadAsync:" +Time.frameCount); // frame:2 return textAsset.text; }
  7. public class MyAwaiter<T> : INotifyCompletion { bool IsCompleted { get;

    } T GetResult(); void OnCompleted(Action continuation); }
  8. // var result = await foo; は以下のようになる if (awaiter.IsCompleted) {

    // もし例外があればGetResultで再throwされる var result = awaiter.GetResult(); // ...awaitの先が実行される } else { // 継続を登録(実際は最適化されてるので毎回ラムダ式は使いません) awaiter.OnCompleted(() => { // もし例外があればGetResultで再throwされる var result = awaiter.GetResult(); // ...awaitの先が実行される }); return; }
  9. 全てのcontinuationを生成しない public async Task FooBarBazAsync() { await Task.Yield(); Console.WriteLine("foo"); await

    Task.Yield(); Console.WriteLine(“bar"); await Task.Yield(); Console.WriteLine("baz"); }
  10. 全てのcontinuationを生成しない public async Task FooBarBazAsync() { await Task.Yield(); Console.WriteLine("foo"); await

    Task.Yield(); Console.WriteLine(“bar"); await Task.Yield(); Console.WriteLine("baz"); }
  11. Unity is (概ね)シングルスレッド C++のエンジンレイヤー + C#スクリプティングレイヤー C#側での扱いはほとんどシングルスレッド (コルーチン, WWW, AsyncOperation,

    etc…) Taskによるasync/awaitは油断するとすぐスレッドプールに飛ばす -> Delay, ContinueWith, Run, etc… async/await(Task)にはマルチスレッド -> シングルスレッドに統合す る機能がついている(SynchronizationContext)、が、そもそもシング ルスレッドなら、その統合レイヤーは消したほうが性能も扱いやす さも上がるのではないか?
  12. Unity is (概ね)シングルスレッド C++のエンジンレイヤー + C#スクリプティングレイヤー C#側での扱いはほとんどシングルスレッド (コルーチン, WWW, AsyncOperation,

    etc…) Taskによるasync/awaitは油断するとすぐスレッドプールに飛ばす -> Delay, ContinueWith, Run, etc… async/await(Task)にはマルチスレッド -> シングルスレッドに統合す る機能がついている(SynchronizationContext)、が、そもそもシング ルスレッドなら、その統合レイヤーは消したほうが性能も扱いやす さも上がるのではないか?
  13. IEnumerator FooCoroutine(Func<int> resultCallback, Func<Exception> exceptionCallback) { int x = 0;

    Exception error = null; yield return Nanikamatu(v => x = v, ex => error = ex); if (error == null) { resultCallback(x); } else { exceptionCallback(error); } } UniTask<int> FooAsync() { var x = await NanikasuruAsync(); return x; }
  14. IEnumerator FooCoroutine(Func<int> resultCallback, Func<Exception> exceptionCallback) { int x = 0;

    Exception error = null; yield return Nanikamatu(v => x = v, ex => error = ex); if (error == null) { resultCallback(x); } else { exceptionCallback(error); } } UniTask<int> FooAsync() { var x = await NanikasuruAsync(); return x; }
  15. 性能のためのUniTask + async/await UniTaskはUnityに特化することでTaskより遥かに性能が良い No ExecutionContext, No SynchronizationContext UniTaskはコルーチン実装よりもアロケーションが少ない 非同期部分ではUniRx(Observable)よりも性能が良い

    使い勝手のためのUniTask + async/await シングルスレッド前提のためマルチスレッド的な落とし穴がない 豊富な機能を提供し、コルーチンをほぼ置き換え可能 UniTask Trackerにより、UniTaskのリークを簡単に回避可能 TaskやRxと混ぜて使うことも問題ない
  16. 性能のためのUniTask + async/await UniTaskはUnityに特化することでTaskより遥かに性能が良い No ExecutionContext, No SynchronizationContext UniTaskはコルーチン実装よりもアロケーションが少ない 非同期部分ではUniRx(Observable)よりも性能が良い

    使い勝手のためのUniTask + async/await シングルスレッド前提のためマルチスレッド的な落とし穴がない 豊富な機能を提供し、コルーチンをほぼ置き換え可能 UniTask Trackerにより、UniTaskのリークを簡単に回避可能 TaskやRxと混ぜて使うことも問題ない
  17. public enum AwaiterStatus { /// <summary>The operation has not yet

    completed.</summary> Pending = 0, /// <summary>The operation completed successfully.</summary> Succeeded = 1, /// <summary>The operation completed with an error.</summary> Faulted = 2, /// <summary>The operation completed due to cancellation.</summary> Canceled = 3 } ValueTaskSourceStatusに合わせています。Taskは本当 に不要なゴミが多くて……)
  18. public async UniTask<int> FooAsync() { await UniTask.Yield(); return 10; }

    public enum AwaiterStatus { /// <summary>The operation has not yet completed.</summary> Pending = 0, /// <summary>The operation completed successfully.</summary> Succeeded = 1, /// <summary>The operation completed with an error.</summary> Faulted = 2, /// <summary>The operation completed due to cancellation.</summary> Canceled = 3 }
  19. public async UniTask<int> FooAsync() { await UniTask.Yield(); return 10; }

    public enum AwaiterStatus { /// <summary>The operation has not yet completed.</summary> Pending = 0, /// <summary>The operation completed successfully.</summary> Succeeded = 1, /// <summary>The operation completed with an error.</summary> Faulted = 2, /// <summary>The operation completed due to cancellation.</summary> Canceled = 3 }
  20. public async UniTask<int> FooAsync() { await UniTask.Yield(); return 10; }

    public enum AwaiterStatus { /// <summary>The operation has not yet completed.</summary> Pending = 0, /// <summary>The operation completed successfully.</summary> Succeeded = 1, /// <summary>The operation completed with an error.</summary> Faulted = 2, /// <summary>The operation completed due to cancellation.</summary> Canceled = 3 }
  21. public async UniTask<int> FooAsync() { await UniTask.Yield(); throw new System.Exception("Error");

    } public enum AwaiterStatus { /// <summary>The operation has not yet completed.</summary> Pending = 0, /// <summary>The operation completed successfully.</summary> Succeeded = 1, /// <summary>The operation completed with an error.</summary> Faulted = 2, /// <summary>The operation completed due to cancellation.</summary> Canceled = 3 }
  22. public async UniTask<int> FooAsync() { await UniTask.Yield(); throw new System.Exception("Error");

    } public enum AwaiterStatus { /// <summary>The operation has not yet completed.</summary> Pending = 0, /// <summary>The operation completed successfully.</summary> Succeeded = 1, /// <summary>The operation completed with an error.</summary> Faulted = 2, /// <summary>The operation completed due to cancellation.</summary> Canceled = 3 }
  23. public async UniTask<int> FooAsync() { await UniTask.Yield(); throw new OperationCanceledException();

    } public enum AwaiterStatus { /// <summary>The operation has not yet completed.</summary> Pending = 0, /// <summary>The operation completed successfully.</summary> Succeeded = 1, /// <summary>The operation completed with an error.</summary> Faulted = 2, /// <summary>The operation completed due to cancellation.</summary> Canceled = 3 }
  24. public async UniTask<int> FooAsync() { await UniTask.Yield(); throw new OperationCanceledException();

    } public async UniTask<int> BarAsync() { var x = await FooAsync(); return x * 2; } public void Baz() { BarAsync().Forget(); }
  25. public async UniTask<int> FooAsync() { await UniTask.Yield(); throw new OperationCanceledException();

    } public async UniTask<int> BarAsync() { var x = await FooAsync(); return x * 2; } public void Baz() { BarAsync().Forget(); }
  26. public async UniTask<int> BarAsync() { try { var x =

    await FooAsync(); return x * 2; } catch (Exception ex) when (!(ex is OperationCanceledException)) { return -1; } }
  27. public async UniTask<int> BarAsync() { try { var x =

    await FooAsync(); return x * 2; } catch (Exception ex) when (!(ex is OperationCanceledException)) { // なんか復旧不能な例外なのでダイアログ出してタイトルに戻る的なことをするとして DialogService.ShowReturnToTitleAsync().Forget(); // fire and forget的に処理 // 元の呼びもとにはキャンセルの連鎖扱いで全てすっ飛ばして終了させる throw new OperationCanceledException(); } }
  28. キャンセル = OperationCanceledException cancellationToken.IsCancelationRequestedをユーザーコードが チェックする必要はない 何故ならユーザーコード部分は同期だから OperationCanceledExceptionを投げるのは非同期の源流 = asyncOperation.ConfigureAwait(token), UniTask.Delay(token),

    etc… とにかく渡す、それだけでいい 非同期の源流が処理してくれるはずなので、そこまで届ければOK まぁそれが面倒くさいんですけどね!!! (性能を落とさず)自動でやってくれる手段はどうハックしてもない
  29. UnityだとMonoBehaviour/OnDestroyが便利 public class FooBehaviour : MonoBehaviour { CancellationTokenSource cts; void

    Start() { cts = new CancellationTokenSource(); } void OnDestroy() { cts.Cancel(); cts.Dispose(); } }
  30. なんとEventをasync/awaitで実装できる await button.OnClickAsync(); await gameObject.OnCollisionEnterAsync(); というための実装がUniTaskには入ってる async UniTask TripleClick(CancellationToken token)

    { // 都度OnClick/token渡しするよりも最初にHandlerを取得するほうが高効率 using (var handler = button.GetAsyncClickEventHandler(token)) { await handler.OnClickAsync(); await handler.OnClickAsync(); await handler.OnClickAsync(); Debug.Log("Three times clicked"); }
  31. パフォーマンスのために ローカル変数では再利用が許されてる(一部の非同期ソースのみ) async UniTask DelayFiveAsync1() { for (int i =

    0; i < 5; i++) { // 毎回Delayを生成している await UniTask.Delay(i * 1000); Debug.Log(i); } } async UniTask DelayFiveAsync2() { // Delayを再利用する var delay = UniTask.Delay(i * 1000); for (int i = 0; i < 5; i++) { await delay; Debug.Log(i); } }
  32. パフォーマンスのために ローカル変数では再利用が許されてる(一部の非同期ソースのみ) async UniTask DelayFiveAsync1() { for (int i =

    0; i < 5; i++) { // 毎回Delayを生成している await UniTask.Delay(i * 1000); Debug.Log(i); } } async UniTask DelayFiveAsync2() { // Delayを再利用する var delay = UniTask.Delay(i * 1000); for (int i = 0; i < 5; i++) { await delay; Debug.Log(i); } }
  33. Don’t be afraid! 性能も問題ない(UniTaskを使えば) プラクティスも既に確立されている(UniTaskを使えば) やりすぎてしまう害もあまりない(強いて言えば非同期汚染) やらない理由がないレベルなので、今すぐGO Recommend to use

    with UniRx.Async Unityのために性能/使い勝手ともに再デザインされたasync/await Unityで使うあらゆるものがawaitできるようになる 標準のTaskを使わないことを恐れないで! あらゆる言語、そして通常の.NETも超えた最先端を行きましょう!