57
54

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

UniTask 2.x を使った堅牢な非同期処理への移行ガイド

Last updated at Posted at 2021-05-27

はじめに

会社でプロジェクトにUniTaskを導入しはじめて数か月ほど経過して、利用した感触や、いろいろなケースの使い方がおぼろげながらまとまってきたのでこちらで紹介することとします。既存のコードはコルーチンやコールバック方式で書かれていたため、そういったコードからの切り替え・相互呼び出しなどが必要になりましたので、そういった点についても紹介します。

常に意識するべき基本的な機能や、既存コードの移行に必要なことを書きますが、応用的な使い方や細かい機能については今回はカバーしません。また、生のTask の利用についても記述しません。

結論

Unityで堅牢な非同期コードを書きたい場合は、UniTaskを使うのが良いです。
導入には一定の知識が必要ですが、この記事でそういった点について紹介します。

履歴

  • 2024/12/6: AutoResetUniTaskCompletionSource とそのリスクについて の項目を追加
    • 従来、new UniTaskCompletionSource() ではなく、AutoResetUniTaskCompletionSource.Create() を利用する方法を紹介していましたが、リスクの低い new UniTaskCompletionSource() を推奨するとともに、リスクについて記載しました。

用語

  • コルーチン

    • Unityの機能としてのコルーチン、およびIEnumeratorを返却するメソッドを指します。
  • UniTaskメソッド

    • この記事では、async UniTask Abcde() { ... } のような形式の、async指定があり、UniTask または UniTaskVoid を返却するメソッドのことをUniTaskメソッドと呼びます。
    • UniTaskメソッドを実行するとUniTaskインスタンスを返却しますが、この時点ではメソッド内に記述されている内容を実行していません。UniTaskインスタンスに対して await を実行したり、.Forget() を実行することでメソッド内の処理が非同期的に実行されます。(UniTaskVoidは除く)
  • UniTask

    • 紛らわしくてすみませんが、複数のケースがあります。
      • UniTaskインスタンス
        • 上記のようなUniTaskメソッドが返す非同期処理オブジェクト
      • UniTaskクラス
        • class UniTask のこと
      • UniTaskライブラリ
        • UniTask機能を提供するライブラリの名前

UniTaskの使いはじめ方

簡単に触れておきます。

  • 最低限のUnityバージョン:

    • 2018.4.13f1 (Unity2017では利用できません)
  • インストール方法

    • Unity Package Managerから左上の + のところの Add package from git URL... をクリックし、https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask を入力する。
    • image.png
  • UniTaskを利用するには、各 .cs ファイルの先頭に以下のusingが必要です。(通常はIDEが補完で追加してくれます)System.Threading は、CancellationTokenのために必要です。

    using Cysharp.Threading.Tasks;
    using System.Threading;
    

主な情報源

  • UniTask公式サイト

    • https://github.com/Cysharp/UniTask
    • 簡潔ではありますが、ここのREADMEに大概のことは書いてあり、また情報も新しいので一番先にあたるべきです。
  • スライド 「UniTaskの使い方2020」(とりすーぷさん)

  • 書籍「UniRx/UniTask完全理解」 (打田恭平著)

    • リファレンスに良いです。
  • スライド「Deep Dive async/await in Unity with UniTask(UniRx.Async)」(neueccさん)

  • 注意点

    • Google検索した場合、2.0より前のUniTaskの記事が表示されがちなので注意されましょう。

UniTaskとは

簡単にだけ書きますが、基本的な仕組みとしては、書き方の違いを除けば、UniTask/Task は、コルーチンと極めて似たものです。非同期処理を直列的、手続き的に書くことができます。しかしいくつかの点で仕様が異なっているため、適用できる用途が違ってきます。このあとそのような違いについて書きます。
なお、TaskとUniTaskの違いなどは「Deep Dive async/await in Unity with UniTask(UniRx.Async)」に詳しいです。

UniTaskの良いところ(コルーチンと比較して)

値を返せるのが便利

コルーチンでは、実行終了後に値を返す良い手法がなく、コールバックが利用されていたと思います。あるいは、IEnumerator.Current を利用して yield return 値; としてからコルーチン終了することで、Currentから値を取り出すような手法もありますが、記述量が多い手法で、見た目にも複雑で、さらに型を制約できないなど問題がありました。UniTaskでは return 値 のように、通常のメソッドと同様の記法で値を返すことができ、返り値の型も通常のように制約されます。

例外を伝播できるのが便利

コルーチンの場合、ネストして実行時に例外を起こすとそのコルーチン内で終了してしまい、呼び出し元のコルーチンには伝播しません(※)。例外を利用したい場合、コルーチンごとにtry~catchを書いたうえで、コールバックによって呼び出し元に伝えるといった手法が通常かと思います。IEnumerator.Currentを利用して呼び出し元に返す手法もありますが、かなり記述量が多くおすすめできません。UniTaskでは、通常のメソッドと同様に呼び出し元に伝播しますので通常のメソッドのように書けます。ただし、注意点があり、キャンセル時に投げられる OperationCanceledException をむやみにキャッチしてしまわないための配慮は必要です。(「例外処理の注意点」の項目参照)

※一度もyieldする前に発生した例外については呼び出し元に伝播するケースがありますが、特殊な条件なので、この挙動に頼ることはできないでしょう。

finally や IDisposable による終了処理が正確に実行できる

終了処理が必要なコードがあった場合、コルーチンの場合は、ネスト実行時、前項で書いたように例外が伝播しないためfinallyブロックが動作しませんでした。また、MonoBehaviourが破壊された場合などもその場で停止してしまうために、finallyブロックが呼ばれません。UniTaskではfinallyブロックが必ず呼ばれるので、リソースの開放が必須な場合などに、より安全なプログラムを書くことができます。

また、IDisposable と using ステートメントを利用した場合でも、UniTaskであれば必ずDispose()が呼ばれます。

finally 利用時の例

async UniTask FooTask(CancellationToken token)
{
  try
  {
    var res = AllocateSomeResource(); // 何らかのリソースを取得
    await BarTask(res, token); // BarTask内で例外が起こるケースを考える
  }
  finally
  {
    ReleaseSomeResource(res); // UniTaskでは、例外時にもここを必ず通るので確実にリソースを開放できる
  }
}

IDisposable 利用時の例

async UniTask FooTask(CancellationToken token)
{
  using(var res = new FooDisposable()) { // FooDisposableはIDisposableを継承しているとする
    await BarTask(res, token); // BarTask内で例外が起こるケースを考える
  }
  // 例外発生時、res.Dispose() がUniTaskでは確実に呼ばれる。
}

別スレッドも使えるのが便利

UniTaskは(C#標準のTaskと違って)通常はメインスレッドで動作しますが、await時の指定で実行スレッドを切り替えることができます。
await UniTask.SwitchToThreadPool() のようにするとそこから別スレッドで動かすことができ、さらに await UniTask.SwitchToMainThread() とすればそこから再度メインスレッドに戻ることができます。非常に簡潔な形で別スレッドを利用できるのが便利です。※ただし、UnityのAPIはほとんどが別スレッドでの動作に対応していないのでご注意ください。


public class FooBehaviour : MonoBehaviour
{
    void Start()
    {
        var token = this.GetCancellationTokenOnDestroy();
        UniTask.Action(async () => {
            var result = await HogeWorkerAsync(10000, token);
            Debug.Log("result:" + result);
        })();
    }

    // 別スレッドに仕事をさせる非同期メソッド
    async UniTask<string> HogeWorkerAsync(int count, CancellationToken token)
    {
        // スレッドを切り替える
        await UniTask.SwitchToThreadPool();

        // ここに書いた内容は別スレッドで実行される(Unity APIはほとんど利用できないことに注意)
        var result = "";
        for (int i = 0; i < count; i++)
        {
            result += i + "|";
        }
        Debug.Log($"This is not main thread. thread: {Thread.CurrentThread.Name}"); // Debug.Logは例外的に別スレッドでも実行可能

        // メインスレッドに戻る
        await UniTask.SwitchToMainThread(cancellationToken: token);
        return result;
    }
}

UniTaskのそうでもないところ

シンプルで返り値もないケースでのタイプ量がやや多い(主にCancellationTokenのため)

コルーチンではMonoBehaviourが破壊された場合に動作が止まりますが、これをUniTaskで実現するにはCancellationTokenの実装が必要で、このために引数リストが長くなってタイプ量が増えます。コルーチンをUniTaskですべて置き換えることも可能ではありますが、GameObjectがらみのウエイトやアニメーションのようなケースでは、コルーチンで十分なことも比較的多いと思いました。

シンプルなケース(コルーチン)

// ちょっと待ってからなにかする (コルーチン)
IEnumerator FooCoroutine() {
  yield return new WaitForSeconds(1.0f);
  ...
}

// 呼び出し時
StartCoroutine(FooCoroutine());

シンプルなケース(UniTask)

// ちょっと待ってから何かする (UniTask)
async UniTask FooTask(CancellationToken token) {
  await UniTask.Delay(1000, cancellationToken: token);
  ...
}

// 呼び出し時
var token = this.GetCancellationTokenOnDestroy();
FooTask(token).Forget();

コールスタック情報がわかりにくい

Unityのコンソールのコールスタックを見ても、どこから呼ばれたUniTaskかなどはわかりにくいです。これはコルーチンでも同様のはずですが、UniTaskのほうがコールスタックの数が多くがややこしい感じはします。(本質的ではないですが)

なお、Unity Editor上であれば、Windowメニューから起動できる UniTask Tracker で Enable StackTrace をONにして実行することで、今実行中のUniTaskについて、呼び出し関係のスタックトレースを表示できるようです。

image.png

いつUniTaskを使うべきか

堅牢な非同期実行コードを書きたいとき

例外が伝播せず、finally や usingステートメント が有効に利用できないコルーチンに比べて、UniTaskを使うと堅牢なコードが書きやすいといえます。コールバック方式と比べても、例外処理などを利用してより簡潔に書けるように思います。

非同期処理の終了時に値を返すようなケース

コルーチンでは書きづらいので、コールバックなどを利用していたと思いますが、そういったケースで直列的に書くことができ、よりシンプルに記述することができます。

MonoBehaviourが破壊されても処理を続けたいケース

コルーチンの場合、StartCoroutineを実行したMonoBehaviourが破壊されたり、GameObjectが非Activeになるとコルーチンの実行が止まります。UniTaskでは、何もしなければMonoBehaviourやGameObjectとかかわりなく動き続けることができ、停止タイミングはCancellationTokenの指定によってコントロールすることができます。

コルーチンでも、ずっと生き続けるGameObjectを利用することで長い寿命は実現できますが、そのようなGameObjectを作成せずに実現することができます。

破壊タイミングの違い

コルーチンの場合

コルーチンはMonoBehaviour.StartCoroutine() を呼ぶことで起動しますが、このMonoBehaviorの寿命がコルーチンの寿命と直結しています。以下のケースでコルーチンは自動的に停止します。

  • コルーチンを起動したMonoBehaviourが破壊された場合
  • コルーチンを起動したMonoBehaviourのあるGameObjectが非Activeになるか、破壊された場合

ちなみに、MonoBehaviourがdisabledになった場合は、コルーチンは止まらないようです

コルーチンの側からみると、上記の条件が成立すると、yieldしたっきり処理が再開しない、という挙動になります。このため、終了処理の機会はありません。なにかのリソースを使いはじめた後、確実に後処理をしないといけないケースなどでは、使いにくいといえます。

UniTaskの場合

UniTask は、MonoBeheviour.StartCoroutine() で実行されているわけではないので、MonoBehaviour の寿命とは関係がありません。何もしなければ、途中で停止することはなく、動きつづけるというのがUniTaskの性質になります。

しかし、UIを扱う場合など、MonoBehaviour が破壊された場合は、そのまま動いてほしくないケースが多いと思います。たとえばウエイトした後Textコンポーネントの文字列を書き換えるタスクだった場合、この GameObject が破壊されていれば、実行する意味はないことになりますし、NullReferenceException の発生にもつながります。
このような場合に CancellationToken を利用することで、コルーチンと同様のタイミングでキャンセルしたり、任意のタイミングでキャンセルを行うことができます。

CancellationTokenについて

CancellationTokenとは何かということですが、「キャンセルしたかどうか」を伝えることができるオブジェクト(実際はstruct)ぐらいに思ってもらえると問題ないと思います。

CancellationTokenを利用して、おおよそコルーチンの場合と同様の破壊タイミングにするには、以下のようなコードにします。ちょっと長くなって書くのが面倒になった、と思うかもしれませんが、これがUniTaskの基本形と考えてもらったほうが良いと思います。

※同等と書きましたが、実際には、GetCancellationTokenOnDestroy() で返されるトークンは、GameObjectが非Active化したときには影響を受けないので、コルーチンの場合とは少し異なります。

async UniTask SomeTask(CancellationToken token) {
  // 任意の処理。
  await UniTask.Delay(0, cancellationToken: token); // await時にはCancellationTokenを渡す
}

// UniTask呼び出し時
var token = this.GetCancellationTokenOnDestroy();
SomeTask(token).Forget();

UniTaskの呼び出しがネストする場合は、以下の例のように、引数で得られたtokenを必ず渡していくことが重要です。

async UniTask SomeTask(CancellationToken token) {
  // 任意の処理。
  await FooTask(token); // UniTaskからUniTaskのネスト呼び出し。CancellationTokenを渡す。
}

async UniTask FooTask(CancellationToken token) {
  // 任意の処理。
  await UniTask.Delay(0, cancellationToken: token); // await時にはCancellationTokenを渡す
}

// UniTask呼び出し時
var token = this.GetCancellationTokenOnDestroy();
SomeTask(token).Forget();

CancellationTokenでの、手動でのキャンセル方法

手動でのキャンセル方法は以下のように、CancellationTokenSourceを利用します。
基本的にはUniTaskの呼び出しごとにCancellationTokenSourceごと作りなおすのが良いと思われます。
GetCancellationTokenOnDestroy() と同様の挙動を実現するには、OnDestroy() で Cancel() を呼びます。


public class HogeBehaviour : MonoBehaviour
{
  CancellationTokenSource cts;

  void Start()
  {
    // UniTask呼び出し時
    var cts = new CancellationTokenSource();
    SomeTask(cts.Token).Forget();
  }

  async UniTask SomeTask(CancellationToken token)
  {
    // 任意の処理。
    await UniTask.Delay(0, cancellationToken: token); // await時にはCancellationTokenを渡す
  }

  void Cancel()
  {
    // キャンセル時
    cts.Cancel();
  }

  // 通常は、コルーチンと同様の破壊タイミングを実現するために OnDestroy() でキャンセルを呼んでおく
  // (GetCancellationTokenOnDestroy()で取得したトークンの場合は不要)
  void OnDestroy()
  {
    cts.Cancel();
    cts.Dispose(); // CancellationTokenSourceの後始末が必要
  }
}

キャンセルしないCancellationTokenの渡し方

以下のように、CancellationToken の実体ののかわりにdefaultを渡すと良いです。これは、struct CancellationToken のデフォルト値、つまり new CancellationToken() の値が渡されます。

  void Start()
  {
    SomeTask(default).Forget(); // default を渡す
  }

  async UniTask SomeTask(CancellationToken token)
  {
    await UniTask.Delay(0, cancellationToken: token);
  }

以下でも同じです。

  void Start()
  {
    SomeTask(new CancellationToken()).Forget();
  }

  async UniTask SomeTask(CancellationToken token)
  {
    await UniTask.Delay(0, cancellationToken: token);
  }

また、CancellationTokenを利用しないケースが考慮されたメソッドを書く場合は、 CancellationToken token = default と、デフォルト値を設定しておけば、トークンを指定せずに実行することができます。もちろんこの場合(トークンの引数を省略した場合)、実行を途中でキャンセルされることがなくなります。

  void Start()
  {
    SomeTask().Forget(); // CancellationTokenを渡さない。(デフォルト値を利用)
  }

  async UniTask SomeTask(CancellationToken token = default) // 引数のデフォルトを指定しておく
  {
    await UniTask.Delay(0, cancellationToken: token);
  }

全部のUniTaskメソッドの引数にCancellationTokenをつけるべきか?

上記で書いたように、CancellationToken は、ネストして呼び出すUniTaskメソッドの呼び出しにも同じトークンをつけることで伝播していくものです。伝播させることで、ネストが深くても、キャンセル時に実行を中止することができます。

一方で、CancellationToken を受け取らない非同期メソッドもありえます。このような非同期メソッドをawaitしたとき、await中にキャンセルのフラグがONになっていても、それに気が付くことはできません。キャンセルしないことで、単に実行時間を消費するだけならさほど大きな問題がないケースもありますが、キャンセル後に実行してはいけない処理があった場合、問題が出ますので、以下のように自前でのキャンセルチェックが必要になります。

自前でのキャンセルチェック

async UniTask FooTask(CancellationToken token) {
  // 任意の処理。
  await BarTask(); // CancellationTokenを受け取らないUniTask

  // 自前でのキャンセルチェック。キャンセルフラグがONになっていれば例外 (OperationCanceledException) を投げてくれる。
  token.ThrowIfCancellationRequested();

  // そのほかの処理
  ...
}

このように手間が増えるため、UniTaskメソッドでは、通常はできるだけ引数で CancellationToken を受け取れるようにしておき、ほかのUniTaskメソッドからの呼び出しに備える、というのが基本戦略と考えられます。

また、キャンセルへの応答が難しいケースで CancellationToken を受け取るべきかは悩ましいところです。しかし、上記のように自前のキャンセル判定をしないですむようにするには CancellationToken に対応しているほうが望ましいので、ほかのUniTaskメソッドから呼び出されることが想定される場合は、やはり CancellationToken を受け取れるようにしておくほうが便利と考えられます。

例外処理の注意点

UniTaskでは、通常のメソッドのように try ~ catch で例外処理ができるのが利点、と書きましたが、実は常に考慮すべき点があります。これもキャンセルがらみです。
というのは、キャンセルの処理が OperationCanceledException という例外によって行われることから、適当に Exception 型を指定して try ~ catch などしてしまうと、キャンセルがそこで止まって、呼び出し元に伝播しません。これは通常は望まない挙動ですので、UniTaskメソッド内での try~catch は OperationCanceledException をスルーする必要があり、ほぼ常に以下のようにするのが良い、というか、このようにする必要があります。
もちろん、 OperationCanceledException が呼び出し元にきちんと伝播する書き方であれば問題ありません。

async UniTask FooTask(CancellationToken token)
{
  try
  {
    await BarTask(token);
  }
  catch (Exception e) when (!(e is OperationCanceledException)) // キャンセル時に発行される例外は上位のUniTaskにそのまま伝播させる
  {
    Debug.Log("error: " + e);
  }
}

例外はどこへ行くのか

書いたコードにtry~catch がない場合、UniTask内で発生した例外って結局どこまで遡るのでしょうか?UniTaskメソッドは最初は非asyncのメソッドから呼ばれているので、非asyncのメソッドからさらに上位に伝播するのでしょうか・・・?
実は、Forget()のところで例外をキャッチして、ログに出力するようになっています。
ですので、Warningを無視してForget() を書かないと、例外はログに出ずに消えてしまう・・・と考えていましたが、そうでもないケースがあるようなので、恐縮ですが、要調査です。

逆引き

そもそもUniTaskの呼び出し方

まず最初にUniTaskじゃないところからUniTaskをどう呼び出すか、ということです。

非asyncメソッドから値を返さないUniTaskを呼び出す方法

<UniTaskメソッド名>().Forget(); のように書くことで値を返さないUniTask、あるいは値を返すUniTaskであっても返却値を無視する形で呼び出すことができます。

class HogeBehaviour : MonoBehaviour
{
  void Start() {
    var token = this.GetCancellationTokenOnDestroy();
    FooTask(token).Forget();
  }

  async UniTask FooTask(CancellationToken token)
  {
    ....
  }
}

非asyncメソッドから値を返すUniTaskを呼び出す方法

これが実はちょっとややこしいです。なぜなら、普通のメソッドは非同期的に値を受け取らないからです。ですので、値を受けとりたければ以下のように、まず別のUniTaskで結果を受け取ってそこで値をハンドリングする必要があります。

※ 「値を返すUniTask」を変更できない場合について書いています。

class HogeBehaviour : MonoBehaviour
{
  void Start() {
    var token = this.GetCancellationTokenOnDestroy();
    ReceiveTask(token).Forget();
  }

  // 値を受け取るためのUniTask
  async UniTask ReceiveTask(CancellationToken token)
  {
    var result = await FooTask(token);
    this.result = result;
  }

  // 値を返すUniTask
  async UniTask<bool> FooTask(CancellationToken token)
  {
    ....
  }
}

多少簡潔に書く方法もあります。

class HogeBehaviour : MonoBehaviour
{
  void Start() {
    var token = this.GetCancellationTokenOnDestroy();
    UniTask.Action(async () => {
      this.result = await FooTask(token);
    })();
  }

  // 値を返すUniTask
  async UniTask<bool> FooTask(CancellationToken token)
  {
    ....
  }
}

返り値がないUniTaskの書き方

普通に <> のないUniTaskを書けばよいです。

  async UniTask FooTask(CancellationToken token)
  {
    ....
  }

UniTaskVoidとは何か

上記、返り値がないUniTaskと混同しがちなのがUniTaskVoidです。
UniTaskVoidとは、終了を待つ「ことができない」UniTaskです。
UniTaskVoidを返すメソッドに対してawaitを書くとシンタックスエラーです。ですので、終了を待つ可能性がほぼ考えられないケースに利用しましょう。
後から処理の終了を待ちたくなることは結構あるので、UniTaskVoidを使いたいケースはそう多くないはずです。処理の終了を待ちたいことがありそうであれば、<> のない UniTask を利用しましょう。
UniTaskVoidの場合、Forget() は実際には何もしないメソッドになっていますが、現状では書かないとWarningが出ます。

  async UniTask BarTask() {
    var token = this.GetCancellationTokenOnDestroy();
    // await FooTask(); とはかけない!!
    FooTask(token).Forget();
  }

  async UniTaskVoid FooTask(CancellationToken token)
  {
    ....
  }

Start() をUniTaskにする方法 (UniTaskVoid)

UniTaskVoidは、.Forget() を呼ばなくても、.Forget() を呼んだのと同じ処理になります。これを利用して、MonoBehaviourの Start() メソッドを非同期メソッドとして書くことが可能です。

  public class HogeBehaviour : MonoBehaviour
  {
    async UniTaskVoid Start() {
      await FooAsync();
    }
    
    async UniTask FooAsync()
    {
      ...
    }
  }

※ Start()を async UniTask Start() のように書いた場合に問題があることを予想しましたが、実際にやってみると同様の挙動となったので、違いは不明です。公式サイト では Start() には UniTaskVoid の利用が推奨されています。

別の手法とのブリッジについて

UniTaskからコルーチンを呼んで待つには

UniTask内のawaitでコルーチンを待つことができます。WithCancellation メソッドを併用することで、CancellationTokenを利用してキャンセル時の処理を行うことができます。


public class HogeBehaviour : MonoBehaviour
{
  void Start() {
    var token = this.GetCancellationTokenOnDestroy();
    FooAsync(token).Forget();
  }

  async UniTask FooAsync(CancellationToken token)
  {
    // UniTask内のawaitでCoroutineを待てる
    await BarCoroutine().WithCancellation(token);
    return 1;
  }

  IEnumerator BarCoroutine()
  {
    yield return new WaitForSeconds(2.0f);
  }
}

UniTaskからコルーチンを起動した場合のコルーチンの寿命は、普通とは異なることに注意

UniTaskから呼ばれたコルーチンは、通常のUnityの処理と異なり、UniTaskが駆動しているため、GameObjectがDestroyされたときに自動的に止まるということはありません。そのような挙動にしたい場合は、GetCancellationTokenOnDestroy() で取得したトークンを渡すなど、CancellationTokenの適切な利用が必要になります。
WithCancellation() を利用してCancellationTokenを渡している場合、キャンセル時には、通常コルーチンと同様に、コルーチン内の yield のところでCoroutineの実行が止まります。

コルーチンからUniTaskを呼んで待つには

UniTaskには ToCoroutune() というメソッドがあり、これを利用することができます。

コルーチンからUniTaskを呼んで待つには: キャッチしたい例外がない場合

特にキャッチするべき例外がない場合は、以下のような簡潔な記法も利用できます。

yield return HogeAsync(default).ToCoroutine();

ただし、CancellationToken利用時のキャンセル時にはエラーログが出てしまう問題があるようです。ログが出る以外の実害はないですが、キャンセルしないケースで利用するのが良いと思われます。

コルーチンからUniTaskを呼んで待つには: 例外をキャッチしたい場合

コルーチンには例外を伝播させる方法がありませんので、例外についてはUniTask内で処理する必要があります。このため、現実的には、以下のような形で例外処理するのが良いと思われます。

    IEnumerator FooCoroutine()
    {
        yield return UniTask.ToCoroutine(async () =>
        {
            try
            {
                var token = this.GetCancellationTokenOnDestroy();
                var result = await HogeAsync(token);
            }
            catch (OperationCanceledException e)
            {
                // キャンセル時の例外対応。Coroutine側で対応が必要ならばここで書く
            }
            catch (Exception e)
            {
                // キャンセル以外の例外対応
                Debug.Log($"Exception: {e}");
            }
        });
    }

    async UniTask<int> HogeAsync(CancellationToken token)
    {
        await UniTask.Delay(2000, cancellationToken: token);
        return 5;
    }

コールバック方式のメソッドからUniTaskを呼ぶ方法

UniTask.Action を利用することで、コールバック方式のメソッドからUniTaskを呼び出すことができます。

public class UniTaskCallbackSample : MonoBehaviour
{
    private void Start()
    {
        LoadCB("sample", (result) =>
        {
            Debug.Log("result:" + result);
        });
    }

    // コールバック方式のメソッド
    public void LoadCB(string input, Action<bool> onResult)
    {
        UniTask.Action(async () =>
        {
            var result = await HogeAsync(input, default);
            onResult?.Invoke(result);
        })();
    }

    // 呼びたいUniTaskメソッド
    async UniTask<bool> HogeAsync(string input, CancellationToken token)
    {
        await UniTask.Delay(2000, cancellationToken: token);
        return true;
    }
}

※この例ではキャンセルしないケースになっています。コールバック側にキャンセル機構がある場合は、CancellationTokenSource を利用しましょう。また、例外が発生する可能性がある場合は例外処理も必要です。

UniTaskからコールバック方式のメソッドを呼ぶ方法

UniTaskCompletionSource を利用することで、コールバック方式のメソッドをUniTaskに変換することができます。成功時に TrySetResult を呼ぶことで結果を返し、待ちを終了させることができます。失敗時には TrySetException で例外を渡すことができます。

public class UniTaskCallbackSample : MonoBehaviour
{
    void Start()
    {
        var token = this.GetCancellationTokenOnDestroy();
        SendGetSceneAsync(token).Forget();
    }

    // UniTaskCompletionSource を利用する。
    // これはasync指定がなく、UniTaskを返す「普通のメソッド」。
    // 直接UniTaskのインスタンスを返しているので、awaitで待つことができる。
    public UniTask<bool> SendGetSceneAsync(CancellationToken token)
    {
        var utcs = new UniTaskCompletionSource<bool>();
        HogeCB(input, r => utcs.TrySetResult(r));
        token.Register(() => utcs.TrySetCanceled()); // CancellationToken対応。
        return utcs.Task;
    }

    Action<bool> callback;

    // コールバック方式の非同期メソッドの例
    void HogeCB(string input, Action<bool> onResult)
    {
        callback = onResult;
        Invoke("AfterWait", 3.0f);
    }

    void AfterWait()
    {
        callback(true);
    }
}

AutoResetUniTaskCompletionSource とそのリスクについて

従来、new UniTaskCompletionSource<bool>() ではなく、 AutoResetUniTaskCompletionSource.Create() を利用する方法を紹介していました。AutoResetUniTaskCompletionSource のほうが、メモリアロケーションを抑止できるという効果はあります。しかし、以下の問題があり、特に速度に影響のある箇所以外での利用は推奨しません。

CancellationToken.Registerと併用した場合に事故が多いことから、あまり推奨しません。事故とは何かということですが、TrySetResultなどの実施でリセットがかかりますが、リセット後は同じインスタンスが別用途に利用されることがあります。CancellationToken.Register が機能したままの場合、別用途に転用された後、以前の用途のRegisterで登録したハンドラが発火するケースがあります。このため、TrySetResult, TrySetCancel, TrySetError時に、Registerをきれいに開放する処理を書かなければ動作上のバグが発生します。これは担保するのが容易とはいえないことであるため、レビュー等で十分にコード品質が担保されないケースでの AutoResetUniTaskCompletionSource の利用はあまり推奨しません。

宣伝

株式会社STYLYでは、Unityエンジニアを募集しています!
https://www.wantedly.com/projects/603605

57
54
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
57
54

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?