UniTask 勉強ノート

[toc]

# UniTask 勉強ノート

## 同期処理&非同期処理

- 同期処理　簡単に言うとコードの順番で実行することです。　

  ```c#
  FuncA()
  {
      // FunB の実行完了を待つ
      FuncB();
      // FunB の実行完了したら、FunAを続行する
  }
  
  void FuncB()
  {
  // do some thing
  }
  ```

  ```plantuml
  activate FuncA
  FuncA -> FuncB --++ : Call
  FuncB --> FuncA --++ : Return
  ```

- 非同期処理　簡単に言うとfunA とfunBを同時実行することです

  ```c#
  FuncA() {
      // FunB の実行完了を待たず、そのままFunAも実行する。
      StartCoroutine(FunB());
  }
  
  IEnumerator FuncB()
  {
      // do some thing
      yield return null;
  }
  ```

  ```plantuml
  activate FuncA
  FuncA ->> FuncB ++ : Call
  FuncB --> FuncA -- : Return
  ```

## 非同期処理はマルチスレッドではない

 シングルスレッドでも非同期の処理を実現可能です。代表的な例としてはunityのコルーチンです。コルーチンはただメインスレッドで非同期処理をすろこと。
 c#のTaskではマルチスレッドに関するThreadPoolなどをカプセル化していたクラス。通常ではTaskクラスはasync/awaitで非同期処理を実現する。

## aysnc/awaitとTask

 この二つのキーワードはc#5.0で導入され、本質的には非同期プログラミングを容易にするためにコンパイラが提供する糖衣構文です。unity開発者としてはコルーチンのアップグレード版です。
例

```c#
  //コルーチン バージョン 1sを待つ
  IEnumerator DelayCoroutine()
  {
        Debug.Log("Start");
        yield return new WaitForSeconds(1f);
        Debug.Log("End");
  }

```

```c#
//Async バージョン　1sを待つ
  async void DelayTask()
  {
      Debug.Log("Start");  
      await Task.Delay(1000);
      Debug.Log("End");
  }

```

## async/await とコルーチンの比較して　良い点

1. c#が提供した機能ですから、Monoなしでも使います。

2. 簡単で非同期メソッドの戻り値を取得する。
   例

   ```c#
   //非同期メソッド，最後にStringをreturnする
    async Task<string> DelayTask()
    {
        Debug.Log("Start");
        await Task.Delay(1000);
        Debug.Log("End");
        return "Completed";
    }
   
   ```

3. asyncはどんなメソッドの前にもつけられるので、Unityのイベント関数も当てはまります。

   ```c#
     async void Start()
     {
         var task = DelayTask();
         Debug.Log("非同期実行中..");//非同期結果を待つ
         var str = await task;
         Debug.Log(str);
     }
   ```

4. コールバックのネストを回避する。
   非同期実行中でコールバックメソッドを実行したい場合があります。
   そのコールバックも非同期操作があると、コールバックのネストになります、コードの可読性が低下してしまいます。
   コールバックを小さなコルーチンにして、メインで yield return することもできますが、コルーチンは値を返すことができないので、前のコルーチンで計算した値を使いたい場合は、コールバックを delegate として渡す必要があり、コールバックのネストを避けることはできません。
   しかし、async/awaitは値を返すことができ、コールバックはawaitの順番で実行されるように書き換えることができます。

5. async/await はTry-Catchで例外をキャッチするができます。

## Taskのキャンセル問題

  現在進行中のasync/awaitの非同期処理をキャンセルするのはすこし難しい。
  async/awaitの非同期処理はTaskのインスタンスを依頼している、taskのインスタンスはマルチスレッドの可能性がある、そして、スレッドはOS（オペレーションシステム層）の資源だから、Taskを直接停止することができません。それを解決するには一つパブリック変数flagを作ります、Taskは実行中でflag検査してflagが変化したら、Task内部で停止する。

  ```c#
    bool flag = true

    async void DelayTask()
    {
      while(flag)
      {
        //DoSomething
      }
    }
  ```

  CancellationTokenSource.TokenはC#が提供された。Taskが生成する時、CancellationTokenSource.Tokenを引数として渡したら、これでメインスレッドとこのTaskの繋がりができました。
  TaskにThrowIfCancellationRequestedという「キャンセルシグナル」を設定し、メインスレッドがCancelを使って通知してくるのを待ちます。
  キャンセルが呼ばれると、TaskはOperationCanceledExceptionを投げてTaskの実行を中断し、最後に現在のTaskのStatusのIsCanceledプロパティをtrueにセットします。
  **注意：この例外は必ず処理する。この例外は、Task.Resultを呼び出すことで取得することができます。TaskのExceptionプロパティに問い合わせることがない場合。 あなたのコードは、例外の発生に気づくことはありません,例外をキャッチできなかった場合、GCの時は以下のようなAggregateExceptionを投げつけます。**

  ```c#
     public static void Main(string[] args)
     {
        var tokenSource = new CancellationTokenSource();
        var token = tokenSource.Token;
        try
        {
            var task = Task.Factory.StartNew(() =>
            {
                for (var i = 0; i < 1000; i++)
                {
                    System.Threading.Thread.Sleep(1000);
                    if (token.IsCancellationRequested)
                    {
                        Console.WriteLine("Abort mission success!");
                        return;
                    }
                }
            }, token);
        }
        catch (AggregateException ex)
        {
            foreach (Exception inner in ex.InnerExceptions)
            {
                Console.WriteLine(inner.Message);
            }
        }

        token.Register(() =>
        {
            Console.WriteLine("Canceled");
        });
        Console.WriteLine("Press enter to cancel task...");
        Console.ReadKey();
        tokenSource.Cancel();
        // Cancel したら
        // Call OperationCanceledException()
        // Call token.IsCancellationRequested = false,
        Console.ReadKey();
    }

  ```

  例外処理する原因の一つにはTaskは戻り値はあり、途中でキャンセルをすると、後に実行するコードの例外が発生してしまう
  コルーチンはGameObjectをDestroyしたら、停止する、またコルーチン開始する時インスタンスを記録して、キャンセルしたい時はStopCoroutine(Coroutine)を実行すればいい。

## UniTask

Unity2017以上はTaskは使用可能ですか、Taskはちょっと重い、そしてUnityに一般的に使用される機能は自分でパッケージしなければなりません。そこでUnitaskが登場する。

  - UnityTaskの良い点：
    1. 使い方はTaskと似ている。
    2. Taskより軽量化、メモリ使用量はすくない
    3. async/awaitの最適化により、大幅なGC削減を実現。
    4. Unity関連機能を提供
    5. 各種Awaiterを提供
    6. UniTaskTracker　Editorでunitaskの可視化

UnityTaskを使用にはUnity2018以上が必要です。
同じUnitaskインスタンスを二回awaitしたら、エラーがでます。

### UniTaskを生成する方法

   1. Taskと同じ

      ```c#
         Task       ->　UniTask
         Task<T>    ->　UniTask<T>
         void       -> UniTaskVoid  //戻り値が不要です
      ```

   2. UniTaskCompletionSource を使用します 

      ```c#
          async void Start()
          {
              var source = new UniTaskCompletionSource();
              ReadyForCompleted(source).Forget();//実行して、実行完了しているかForgotする
              Debug.Log("Do Something...");
              source.TrySetResult();//設置完成
              //source.TrySetException(Exception);//設置失敗
              //source.TrySetCanceled();//設置キャンセル
              Debug.Log("Completed");
          }
      
          async UniTask ReadyForCompleted(UniTaskCompletionSource source)
          {
              Debug.Log("Wait");
              await source.Task;
              Debug.Log("Completed");
          }
      ```

         これは完了するかどうか、例外かキャンセルかを手動で設定することができるTaskです。戻り値を設定できる汎用クラスUniTaskCompletionSource＜T＞もあります。

         TrySetメソッドの1つが実行されると、そのインスタンスの他のTrySetメソッドの実行が無効になる。
         **注意：これで生成されたUniTaskは複数回await可能。**

3. AutoResetUniTaskCompletionSource.Create() 
   UniTaskCompletionSourceのpool版です。これで生成されたUniTaskは一回しかawaitができません。ローカルスコープでの使用に適しており、そのまま捨てることができます。

### UniTaskでよく使われる静的メソッド

1. UniTask.Run(Action)/ UniTask.Run(Function);
   使用方法はTaskと同じ。

   ```c#
      /// <summary>[Obsolete]recommend to use RunOnThreadPool(or UniTask.Void(async void), UniTask.Create(async UniTask)).</summary>
        public static async UniTask Run(Action action, bool configureAwait = true, CancellationToken cancellationToken = default);
   ```

2. UniTask.Delay

   ```c#
   UniTask.Delay(1000); //Delay1000ms
   UniTask.Delay(TimeSpan.FromSeconds(1));//Delay1s
   UniTask.Delay(1000,  PlayerLoopTiming.FixedUpdate);//FixedUpdate の基準 
   ```

3. UniTask.DelayFrame 

   ```c#
   UniTask.DelayFrame(3);//wait 3 frame defaultは Updteのタイミング
   UniTask.DelayFrame(3, PlayerLoopTiming.FixedUpdate);//wait 3 frame FixedUpdateのタイミング
   ```

4. UniTask.Yield() //wait 1 frame

   ```c#
    await UniTask.Yield();//wait 1 frame Updteのタイミング
    Debug.Log(Time.time);
    await UniTask.Yield(PlayerLoopTiming.FixedUpdate);//wait 1 frame FixedUpdteのタイミング
    //↓↓↓ここからFixedUpdateのタイミングで実行する↓↓↓
    Debug.Log(Time.time);
    await UniTask.Yield();//wait 1 frame，Updateに戻る
    //↓↓↓ここからUpdateのタイミングで実行する↓↓↓
    Debug.Log(Time.time);
   ```

5. UniTask.SwitchToThreadPool / UniTask.SwitchToMainThread　//スレッドを切り替え

   ```c#
   await UniTask.Yield();
   //ここからmainthread 
   await UniTask.SwitchToThreadPool();
   //ここからThreadPool
   await UniTask.SwitchToMainThread();
   //mainthread に戻る
   ```

   **YieldとSwitchToMainThreadの違いは、SwitchToMainThreadはすでにメインスレッドにいる場合は次のフレームを待たないの対し、Yieldはメインスレッドの下にいてもいなくても1フレームは待つという点です。**

6. UniTask.WaitUntil/UniTask.WaitWhile　
   //コルーチンのWaitUtil/WaitWhileと似ている。Checkタイミングを指定可能

  ```c#
  await UniTask.WaitUntil(()=> isActiveAndEnabled,PlayerLoopTiming.FixedUpdate);

  await UniTask.WaitWhile(() => mValue == false);
  ```

7. UniTask.WaitUntilValueChanged //指定Objectの値が変化した時

   ```c#
   var str = await UniTask.WaitUntilValueChanged(this.transform,x =>x.position);// 第1パラメータは判定対象、第2パラメータは判定メソッドのデリゲートです。この戻り値が変化した場合、変更があったことを意味する
   Debug.Log(str);
   ```

8. UniTask.WhenAll(List) //Task.WhenALlと同じ、すべてのTaskが完成したら、returnする。Unitaskは異なるタイプの戻り値を返すことができる

   ```c#
   var num = UniTask.Run(()=>1);
   var fl = UniTask.Run(()=>0.5f);
   var str = UniTask.Run(()=>"aa");
   var (p1, p2, p3) = await UniTask.WhenAll(num, fl, str);
   ```

9. UniTask.WhenAny(List)　//Task.WhenAny()と同じ、Listの中の一つTaskが完成すればreturn。

   ```c#
     /// <summary>
     /// 最初のPingのIPAddress を返します。
     /// </summary>
     /// <param name="apiHost"></param>
     /// <returns></returns>
     private async UniTask<IPAddress> SelectHostAsync(IPAddress[] apiHost)
     {
         var tasks = apiHost.Select(PingAsync).ToArray();
         var (_, result) = await UniTask.WhenAny(tasks);
         return result;
     }
   
     private async UniTask<IPAddress> PingAsync(IPAddress iP)
     {
         var ping = new Ping(iP.ToString());
         while (!ping.isDone)
         {
               await UniTask.Yield();
         }
         return iP;
     }
   ```

10. UniTask.Create<T>(Function(UniTask<T>)) //UniTaskを素早く生成する方法、

    ```c#
    UniTask.Create(
      async ()=> 
      {
        Debug.Log("Create");
        await UniTask.Delay(1000);
        return "11"; 
      });
    ```

11. UniTask.Defer(Function(UniTask<T>)) //UniTaskを素早く生成する、生成の時実行しない、awaitの時、実行します。

    ```c#
    var defer = UniTask.Defer(
        async () => 
        {
                  Debug.Log("defer");
                  await UniTask.Delay(1000);
                  return "defer";
        }
    );
    await defer;
    //await defer;
    //InvalidOperationException: Token version is not matched, can not await twice or get Status after await.
    ```

12. UniTask.Lazy(Function(UniTask<T>))　//AsyncLazy型のオブジェクトを生成、生成の時実行しない、awaitの時、実行します。**Deferとの違うは何回でもawaitできます**

    ```c#
    var asyncLazy = UniTask.Lazy(
      async () =>
      {
        Debug.Log("asyncLazy");
        await UniTask.Delay(1000);
        return "asyncLazy";
      }
    );
    await asyncLazy.Task;
    await asyncLazy.Task;
    ```

13. UniTask.Void(Function(UniTask<T>)) // 直接非同期デリケートを実行

    ```c#
    UniTask.Void(
            async () => 
            {
                Debug.Log("UniTask.Void");
                await UniTask.Delay(1000);
            }
            );
    ```

14. UniTask.Action/UnityAction(Function(UniTask<T>)) // 非同期デリケートをAction/UnityActionへ変換する。

    ```c#
       UniTask.Action(
          async () => 
          {
            Debug.Log("UniTask.Action");
            await UniTask.Delay(1000);
          }
        );
    ```

    ＝

    ```c#
    ()=>
      {
        UniTask.Void(
          async () => 
          {
            Debug.Log("UniTask.Void");
            await UniTask.Delay(1000);
          }
        );
      };
    ```

15. UniTask.Timeout/TimeoutWithoutException() // UniTaskのTimeoutかどうかを監視する。

    ```c#
        //一秒ないで完成しないと例外を発生
        var str = await DelayTask(token).Timeout(TimeSpan.FromSeconds(1));
      
        //一秒ないで完成しないと，await自体は完成
        //IsTimeout = true
        var result = await DelayTask(token).TimeoutWithoutException(TimeSpan.FromSeconds(1));
        
        Debug.Log(result.IsTimeout);
        Debug.Log(result.Result);
      
      　private async UniTask<string> DelayTask(CancellationToken token)
        {
            await UniTask.Delay(TimeSpan.FromSeconds(0.5));
            //await UniTask.Delay(TimeSpan.FromSeconds(1));
            return "完成";
        }
    ```

## Unityオブジェクトの拡張——Awaiter

UnityのObjectにGetAwaiter()機能を追加

1. コルーチンのawait

   ```c#
   async void Start()
   {
       await DelayCoroutine();
   }
   IEnumerator DelayCoroutine()
   {
       Debug.Log("Start");
       yield return new WaitForSeconds(1f);
       Debug.Log("End");
   }
   ```

     UnityTast => コルーチンに変換

   ```c#
   IEnumerator DelayCoroutine()
   {
       Debug.Log("Start");
       yield return UniTask.Delay(1000).ToCoroutine();
       Debug.Log("End");
   }
   ```

2. AsyncOperationのAwaiter

   ```c#
   //AsyncOperationのwait
   await SceneManager.LoadSceneAsync("NextScene");
   //ResourceRequestのwait
   await Resources.LoadAsync<Texture>("Icon").ToUniTask();
   //AssetBundleのwait
   await AssetBundle.LoadFromFileAsync("ABPath");
   //UnityWebRequestAsyncOperationのwait
   var urw = UnityWebRequest.Get("http://unity.com/");
   await urw.SendWebRequest();
   ```

     progressを検査したい場合はProgressのインスタンスが必要です。

   ```c#
     var progress = Progress.Create<float>(f => Debug.Log($"progress：{f}"));
     var urw = UnityWebRequest.Get("http://unity.com/");
     await urw.SendWebRequest().ToUniTask(progress: progress);
   ```

3. uGUIの各種イベントのawait

   ```c#
   private Button _btn;
   private Toggle _toggle;
   private InputField _inputField;
   private Slider _slider;
   async void Start()
   {
       //token
       var token = this.GetCancellationTokenOnDestroy();
       //1回を待つ
       await _btn.OnClickAsync();
       await _toggle.OnValueChangedAsync();
       await _inputField.OnEndEditAsync();
       await _slider.OnValueChangedAsync();
       //何回を待つ
       //Button clicked
       var btnEventHandler = _btn.GetAsyncClickEventHandler(token);
       await btnEventHandler.OnClickAsync();
       //Toggle 状態更新
       var togEventHandler = _toggle.GetAsyncValueChangedEventHandler(token);
       await togEventHandler.OnValueChangedAsync();
       //InputField テキスト入力完了
       var inputEventHandler = _inputField.GetAsyncEndEditEventHandler(token);
       await inputEventHandler.OnEndEditAsync();
       //Slider 値更新
       var sliderEventHandler = _slider.GetAsyncValueChangedEventHandler(token);
       await sliderEventHandler.OnValueChangedAsync();
   }
   ```

4. MonoBehaviourの各種イベントのawait

   ```c#
   //Collision
   var collisionEnterTrigger = this.GetAsyncCollisionEnterTrigger();
   var collisionExitTrigger = this.GetAsyncCollisionExitTrigger();
   var collisionStayTrigger = this.GetAsyncCollisionStayTrigger();
   var enter = await collisionEnterTrigger.OnCollisionEnterAsync();
   var exit = await collisionExitTrigger.OnCollisionExitAsync();
   var stay = await collisionStayTrigger.OnCollisionStayAsync();
   
   //animator
   var animatorIKTrigger = this.GetAsyncAnimatorIKTrigger();
   var animatorMoveTrigger = this.GetAsyncAnimatorMoveTrigger();
   var layerIndex = await animatorIKTrigger.OnAnimatorIKAsync();
       await animatorMoveTrigger.OnAnimatorMoveAsync();
   
   //Visible
   var visibleTrigger = this.GetAsyncBecameVisibleTrigger();
   var InvisibleTrigger = this.GetAsyncBecameInvisibleTrigger();
       await visibleTrigger.OnBecameVisibleAsync();
       await InvisibleTrigger.OnBecameInvisibleAsync();
   ```

     まだたくさんあります。

5. DoTweenのawait　

   //Project Setting Scripting_Define_Symbols UNITASK_DOTWEEN_SUPPORT をいれましたら。Dotweenのawaitは使用可能
   <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2713384/2ee34a9c-9ae9-f4a1-a279-db5f0a34c993.png" alt="e37a1018-1a5d-9237-413b-67a26882f898.png"  />

## 実行中の非同期メソッドをキャンセルします。

1. CancellationToken //元々はC#のTaskをキャンセルするためのグラス

   ```c#
     //Token 生成
     var tokenSource = new CancellationTokenSource();
     var token = tokenSource.Token;
     //Tokenをキャンセルする
     tokenSource.Cancel();
     //Check
     if (token.IsCancellationRequested)
     {
         Debug.Log("Cancel");
     }
     token.ThrowIfCancellationRequested();//tokenキャンセルした場合，OperationCanceledExceptionをThrow。
   ```

     毎回新しいtokenを生成するには面倒だし、Scriptが破棄された時、そのScripts中の非同期メソッドを停止する

   ```c#
   var token2 = this.GetCancellationTokenOnDestroy();
   ```

   　一度キャンセルされたUniTaskはCancel状態になります、まだawait場合、await以後のコードは実行されません。だからtokenはできるだけ非同期メソッドに渡す。渡すことができない場合は手動で判断する

   ```c#
   private async UniTask<string> ReadTxtAsync(string path, CancellationToken token)
   {
       return await UniTask.Run(() => 
       {
           //実行前の確認
           token.ThrowIfCancellationRequested();
           var str = File.ReadAllText(path);
           //実行後の確認
           token.ThrowIfCancellationRequested();
           return str;
       });
   }
   ```

2. OperationCanceledException
   OperationCanceledException が発行されたUniTask は状態がCanceled となる（それ以外の例外ではFaulted）。同時errorLog発生しない
   UniTaskの中でTry-Catchする時　OperationCanceledExceptionを無視する

  ```c#
private async UniTask TaskFunc(CancellationToken token)
{
    try
    {
        await UniTask.Delay(1000, cancellationToken : token);
    }
    catch (Exception e)
    when(!(e is OperationCanceledException))
    {
        Debug.LogError("Error");
    }
}
  ```

## エディタ下でのUniTaskの監視

Window/UniTask Tracker 実行中のUniTaskを確認したり、漏れているUniTaskがないかを確認することができます。
![image-20220621171549860.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2713384/550533f3-bf14-7165-83da-52a284d49e39.png)