7
3

More than 1 year has passed since last update.

UniTask 勉強ノート

Last updated at Posted at 2022-07-08

[toc]

UniTask 勉強ノート

同期処理&非同期処理

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

    FuncA()
    {
        // FunB の実行完了を待つ
        FuncB();
        // FunB の実行完了したら、FunAを続行する
    }
    
    void FuncB()
    {
    // do some thing
    }
    
  • 非同期処理 簡単に言うとfunA とfunBを同時実行することです

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

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

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

aysnc/awaitとTask

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

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

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

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

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

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

    //非同期メソッド,最後にStringをreturnする
     async Task<string> DelayTask()
     {
         Debug.Log("Start");
         await Task.Delay(1000);
         Debug.Log("End");
         return "Completed";
     }
    
    
  3. asyncはどんなメソッドの前にもつけられるので、Unityのイベント関数も当てはまります。

      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内部で停止する。

  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を投げつけます。

   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と同じ

       Task       -> UniTask
       Task<T>    -> UniTask<T>
       void       -> UniTaskVoid  //戻り値が不要です
    
  2. UniTaskCompletionSource を使用します

        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と同じ。

       /// <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

    UniTask.Delay(1000); //Delay1000ms
    UniTask.Delay(TimeSpan.FromSeconds(1));//Delay1s
    UniTask.Delay(1000,  PlayerLoopTiming.FixedUpdate);//FixedUpdate の基準 
    
  3. UniTask.DelayFrame

    UniTask.DelayFrame(3);//wait 3 frame defaultは Updteのタイミング
    UniTask.DelayFrame(3, PlayerLoopTiming.FixedUpdate);//wait 3 frame FixedUpdateのタイミング
    
  4. UniTask.Yield() //wait 1 frame

     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 //スレッドを切り替え

    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タイミングを指定可能

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

await UniTask.WaitWhile(() => mValue == false);
  1. UniTask.WaitUntilValueChanged //指定Objectの値が変化した時

    var str = await UniTask.WaitUntilValueChanged(this.transform,x =>x.position);// 第1パラメータは判定対象、第2パラメータは判定メソッドのデリゲートです。この戻り値が変化した場合、変更があったことを意味する
    Debug.Log(str);
    
  2. UniTask.WhenAll(List) //Task.WhenALlと同じ、すべてのTaskが完成したら、returnする。Unitaskは異なるタイプの戻り値を返すことができる

    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);
    
  3. UniTask.WhenAny(List) //Task.WhenAny()と同じ、Listの中の一つTaskが完成すればreturn。

      /// <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;
      }
    
  4. UniTask.Create(Function(UniTask)) //UniTaskを素早く生成する方法、

    UniTask.Create(
      async ()=> 
      {
        Debug.Log("Create");
        await UniTask.Delay(1000);
        return "11"; 
      });
    
  5. UniTask.Defer(Function(UniTask)) //UniTaskを素早く生成する、生成の時実行しない、awaitの時、実行します。

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

    var asyncLazy = UniTask.Lazy(
      async () =>
      {
        Debug.Log("asyncLazy");
        await UniTask.Delay(1000);
        return "asyncLazy";
      }
    );
    await asyncLazy.Task;
    await asyncLazy.Task;
    
  7. UniTask.Void(Function(UniTask)) // 直接非同期デリケートを実行

    UniTask.Void(
            async () => 
            {
                Debug.Log("UniTask.Void");
                await UniTask.Delay(1000);
            }
            );
    
  8. UniTask.Action/UnityAction(Function(UniTask)) // 非同期デリケートをAction/UnityActionへ変換する。

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

    ()=>
      {
        UniTask.Void(
          async () => 
          {
            Debug.Log("UniTask.Void");
            await UniTask.Delay(1000);
          }
        );
      };
    
  9. UniTask.Timeout/TimeoutWithoutException() // UniTaskのTimeoutかどうかを監視する。

        //一秒ないで完成しないと例外を発生
        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

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

    UnityTast => コルーチンに変換

    IEnumerator DelayCoroutine()
    {
        Debug.Log("Start");
        yield return UniTask.Delay(1000).ToCoroutine();
        Debug.Log("End");
    }
    
  2. AsyncOperationのAwaiter

    //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のインスタンスが必要です。

      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

    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

    //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は使用可能
    e37a1018-1a5d-9237-413b-67a26882f898.png

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

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

      //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中の非同期メソッドを停止する

    var token2 = this.GetCancellationTokenOnDestroy();
    

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

    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を無視する

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

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3