$30 off During Our Annual Pro Sale. View Details »

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

Yoshifumi Kawai
September 15, 2018
120

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

Yoshifumi Kawai

September 15, 2018
Tweet

More Decks by Yoshifumi Kawai

Transcript

  1. View Slide

  2. 河合 宜文 / Kawai Yoshifumi / @neuecc
    New World, Inc.
    C#
    Unity

    View Slide

  3. C#最速シリアライザ
    https://github.com/neuecc/MessagePack-CSharp/

    View Slide

  4. Reactive Extensions for Unity
    https://github.com/neuecc/UniRx/
    async/await(UniTask)
    async UniTask DemoAsync()
    {
    // You can await Unity's AsyncObject
    var asset = await Resources.LoadAsync("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 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"

    View Slide

  5. View Slide

  6. using C

    View Slide

  7. in 10 years

    View Slide

  8. Rx vs async/await

    View Slide

  9. Rxは非同期に適用可能なものでは?
    IObservable time
    event
    async
    IE

    View Slide

  10. 非同期を同期的に扱う仕組み
    static string GetSync(int page)
    {
    try
    {
    var url = "http://...?page=" + page;
    var html = GetHttpStringSync(url);
    return html;
    }
    catch
    {
    return "Error";
    }
    }
    static async Task GetAsync(int page)
    {
    try
    {
    var url = "http://...?page=" + page;
    var html = await GetHttpStringAsync(url);
    return html;
    }
    catch
    {
    return "Error";
    }
    }

    View Slide

  11. 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 IObservable
    T Task

    View Slide

  12. IObservableで全てを表せること

    View Slide

  13. 複雑な制御に実は向かない

    View Slide

  14. 複雑な制御に実は向かない

    View Slide

  15. 非同期はasync/await、イベントはRx

    View Slide

  16. What is the aysnc/await

    View Slide

  17. async/awaitはマルチスレッド……
    ではない!!!

    View Slide

  18. 非同期はマルチスレッドではない
    これは口を酸っぱくしてしつこく言わねばならぬ
    マルチスレッドになる場合もある、ぐらいが正しい
    コルーチンはマルチスレッドではないでしょう?
    JavaScriptはマルチスレッドではないでしょう?
    でもTaskはマルチスレッドでしょ?
    Yes, でもありNo, でもある
    Taskが元々マルチスレッドのものであり、それが流用されているた
    め、挙動的にもマルチスレッドになる場合も多く誤解を生みやすい

    View Slide

  19. View Slide

  20. View Slide

  21. 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の独自進化で返済していってる

    View Slide

  22. 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の独自進化で返済していってる

    View Slide

  23. How work async/await

    View Slide

  24. async Task SampleTextLoadAsync()
    {
    Debug.Log("Before LoadAsync:" +Time.frameCount); // frame:1
    var textAsset = await Resources.LoadAsync("te") as TextAsset;
    Debug.Log("After LoadAsync:" +Time.frameCount); // frame:2
    return textAsset.text;
    }

    View Slide

  25. 手作業 -> 自動化
    callbackの連鎖を、awaitで自動的に生成して動かしてくれる
    例外の伝搬や最適化などもしてくれるので、手作業でやるよりも効
    率が良い場合もある
    async/awaitはコルーチン?
    本質的な意味はCPS変換、実装の詳細はステートマシン
    最適化のための実装としてコルーチンが選ばれてるだけなので、そ
    こを突っついてもそんな意味はない

    View Slide

  26. 非同期 is not 非同期
    asyncは上位に伝搬する(asyncを呼ぶメソッドはasyncになる)
    結果、asyncだけど中身が同期の場合もよくある
    それを毎回continuationを呼ぶ(デリゲート経由で呼び出し)するの
    は、デリゲートゴミ生成+呼び出しコストがあってよろしくない

    View Slide

  27. public class MyAwaiter : INotifyCompletion
    {
    bool IsCompleted { get; }
    T GetResult();
    void OnCompleted(Action continuation);
    }

    View Slide

  28. // 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;
    }

    View Slide

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

    View Slide

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

    View Slide

  31. Why UniTask

    View Slide

  32. UniRx.Asyncの主要クラス
    C# 7.0からasyncの戻り値をTask以外で実装できるようになった
    それを利用した独自のasync対応型で構造体のTask(ValueTask相当)
    つまりオレオレ非同期フレームワーク
    C# 7.0はIncremental Compilerか、Unity 2018.3で利用可能
    なぜ必要か
    全てを差し替えることでTask自体の負債を完全に無視する
    Unity自体が特殊な実行環境なので特化することで最速を実現する

    View Slide

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

    View Slide

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

    View Slide

  35. XxxContext is the overhead of Task
    ExecutionContextとSynchronizationContextの二種類のキャプチャ

    View Slide

  36. XxxContext is the overhead of Task
    ExecutionContextとSynchronizationContextの二種類のキャプチャ

    View Slide

  37. コルーチンを置き換えるためのユーティリティ
    UniTask.Delay
    UniTask.WaitUntil
    UniTask.WaitWhile
    UniTask.WaitUntilValueChanged
    UniTask.Run
    UniTask.Yield
    UniTask.SwitchToMainThread
    UniTask.SwitchToThreadPool
    await AsyncOperation

    View Slide

  38. IEnumerator FooCoroutine(Func resultCallback, Func 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 FooAsync()
    {
    var x = await NanikasuruAsync();
    return x;
    }

    View Slide

  39. IEnumerator FooCoroutine(Func resultCallback, Func 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 FooAsync()
    {
    var x = await NanikasuruAsync();
    return x;
    }

    View Slide

  40. View Slide

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

    View Slide

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

    View Slide

  43. State of UniTask

    View Slide

  44. public enum AwaiterStatus
    {
    /// The operation has not yet completed.
    Pending = 0,
    /// The operation completed successfully.
    Succeeded = 1,
    /// The operation completed with an error.
    Faulted = 2,
    /// The operation completed due to cancellation.
    Canceled = 3
    }
    ValueTaskSourceStatusに合わせています。Taskは本当
    に不要なゴミが多くて……)

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  54. public async UniTask 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();
    }
    }

    View Slide

  55. Cancellation of async

    View Slide

  56. キャンセル is 面倒
    Rxは戻り値のIDisposableをワンパンすれば良かったのに!
    (代わりにasync/awaitにはIDisposableというアロケーションはない)
    かわりに引数に(CancellationTokenを渡して回る)
    public Task FooAsync(int x, int y, CancellationToken cancellationToken = default)
    {
    var x = await BarAsync(x, y, cancellationToken);
    return x;
    }

    View Slide

  57. キャンセル = OperationCanceledException
    cancellationToken.IsCancelationRequestedをユーザーコードが
    チェックする必要はない
    何故ならユーザーコード部分は同期だから
    OperationCanceledExceptionを投げるのは非同期の源流
    = asyncOperation.ConfigureAwait(token), UniTask.Delay(token), etc…
    とにかく渡す、それだけでいい
    非同期の源流が処理してくれるはずなので、そこまで届ければOK
    まぁそれが面倒くさいんですけどね!!!
    (性能を落とさず)自動でやってくれる手段はどうハックしてもない

    View Slide

  58. UnityだとMonoBehaviour/OnDestroyが便利
    public class FooBehaviour : MonoBehaviour
    {
    CancellationTokenSource cts;
    void Start()
    {
    cts = new CancellationTokenSource();
    }
    void OnDestroy()
    {
    cts.Cancel();
    cts.Dispose();
    }
    }

    View Slide

  59. 高コストでは?
    Yes!
    キャンセルが例外系であったり、たまにの発火なら、そこまで問題
    は出ないはずですが、正常系でのキャンセルが前提になっていると、
    状況によりかなり厳しいことが起こりうる
    例えばシーンのMonoBehaviourに紐づけて、シーン遷移時に画面に
    ある10000個のCubeのキャンセルで例外が発火したら… …?
    UniTask.SuppressCancellationThrow
    UniTaskではキャンセルを(bool isCanceled, T value)に変換する
    SuppressCancellationThrowが用意されている
    ただし例外抑制ができるのは非同期の源流のみなので注意

    View Slide

  60. Async Eventhandling

    View Slide

  61. なんとEventをasync/awaitで実装できる
    await button.OnClickAsync();
    await gameObject.OnCollisionEnterAsync();
    というための実装がUniTaskには入ってる
    async UniTask TripleClick(CancellationToken token)
    {
    await button.OnClickAsync(token);
    await button.OnClickAsync(token);
    await button.OnClickAsync(token);
    Debug.Log("Three times clicked");
    }

    View Slide

  62. なんと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");
    }

    View Slide

  63. まぁ、無理は少しある
    イベントハンドリングはRxのほうが原則的には良い
    コードも長くなるし性能面でも考慮すべき事項がかなり増える
    ただし、複雑なフローを実装する場合には、Rxのオペレーターをや
    りくりするよりも「同期的にイベントを待機するコード」のほうが
    綺麗に素直に書ける可能性がある
    手札として、こういう手法を持っておくことは悪くないでしょう。
    (ReactivePropertyも同じようにawaitできます)

    View Slide

  64. Reusable Promise

    View Slide

  65. パフォーマンスのために
    ローカル変数では再利用が許されてる(一部の非同期ソースのみ)
    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);
    }
    }

    View Slide

  66. パフォーマンスのために
    ローカル変数では再利用が許されてる(一部の非同期ソースのみ)
    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);
    }
    }

    View Slide

  67. Conclusion

    View Slide

  68. Don’t be afraid!
    性能も問題ない(UniTaskを使えば)
    プラクティスも既に確立されている(UniTaskを使えば)
    やりすぎてしまう害もあまりない(強いて言えば非同期汚染)
    やらない理由がないレベルなので、今すぐGO
    Recommend to use with UniRx.Async
    Unityのために性能/使い勝手ともに再デザインされたasync/await
    Unityで使うあらゆるものがawaitできるようになる
    標準のTaskを使わないことを恐れないで!
    あらゆる言語、そして通常の.NETも超えた最先端を行きましょう!

    View Slide