【Unity】UniTaskとasync/awaitを使って非同期処理を実装する【脱コルーチン】

Unity
マイケル
マイケル
みなさんこんにちは!
マイケルです!
エレキベア
エレキベア
こんにちクマ〜〜〜
マイケル
マイケル
今回はUnityの非同期処理ライブラリである
UniTask について触れていきます!
エレキベア
エレキベア
UniTaskって何なのクマ?
マイケル
マイケル
簡単にいうと、Unityでいい感じにasync/awaitの非同期処理を行うためのライブラリで、
これを使うことで脱コルーチンすることができるんだ!
エレキベア
エレキベア
コルーチンの代わりにもなるクマね
それは楽しみクマ〜〜〜
マイケル
マイケル
それじゃ早速みていこう!
スポンサーリンク

UniTaskとは

最適化されたTask

マイケル
マイケル
まず、C#には標準でasync/awaitを行うための Task という機能が用意されているのですが、
これはマルチスレッド前提で作られているため、メインスレッドでしか特定のAPIを実行できないUnityとは相性が悪かったみたいです。
そのような経緯でよりUnityに最適化して作られたライブラリがUniTaskになります。
エレキベア
エレキベア
C#標準にも同じようなのがあるのクマね
マイケル
マイケル
ちなみにTaskは標準だと呼び出す度に実行するスレッドが変わるみたいだけど、
Unityではスレッドが変わらないよう制御はされているみたいです。
そのため使えるには使えるのですが、内部ではスレッド切り替えを行なっているようなので効率が悪いみたいですね・・・。
マイケル
マイケル
この辺りの詳しい話は下記スライドがとても分かりやすいので見てみてください!

エレキベア
エレキベア
中々面白いスライドクマね
マイケル
マイケル
それではTaskについて軽く触れたところでUniTaskに戻りますが、
簡単な例としては下記のようにして非同期処理を実装することができます。
    public class UniTaskSample01 : MonoBehaviour
    {
        private async void Start()
        {
            await HelloWorldTask();
        }
        
        private async UniTask HelloWorldTask()
        {
            await UniTask.Delay(1000);
            Debug.Log("Hello, World");
        }
    }
↑1秒待ってログ出力する処理
マイケル
マイケル
UniTaskを戻り値として定義すると、その関数を非同期処理として定義することができます。
そのタスクをasyncを付けた関数の中でawaitすることで、処理が完了するのを待機することができるというわけです!
エレキベア
エレキベア
async -> 非同期
await -> 待機する

というわけクマね
マイケル
マイケル
Unityのコルーチンと使い方が似ていると感じる人も多いと思います。
比べた時にどのようなメリットがあるのかについて見ていきましょう!

コルーチンとの比較

マイケル
マイケル
Unityのコルーチンは下記のように使うことができます。
using System;
using System.Collections;
using UnityEngine;

namespace UniTaskTest
{
    public class CoroutineSample01 : MonoBehaviour
    {
        private void Start()
        {
            StartCoroutine(HelloWorldCoroutine());
        }

        private IEnumerator HelloWorldCoroutine()
        {
            yield return new WaitForSeconds(1.0f);
            Debug.Log("Hello, World");
        }
    }
}
↑コルーチンの実行
エレキベア
エレキベア
お馴染みの処理クマね
マイケル
マイケル
この処理だけだと先ほどのUniTaskの例とあまり変わらないかもしれませんが、
非同期処理が完了した後に別の処理を行いたい場合はどのように記述すればいいでしょうか?
マイケル
マイケル
これを実装しようとすると、恐らく下記のようになると思います。
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Events;

namespace UniTaskTest
{
    public class CoroutineSample02 : MonoBehaviour
    {
        private void Start()
        {
            StartCoroutine(HelloWorldCoroutine(() => Debug.Log("Finish!!")));
        }

        private IEnumerator HelloWorldCoroutine(UnityAction callback)
        {
            yield return new WaitForSeconds(1.0f);
            Debug.Log("Hello, World");
            callback(); // コールバック実行
        }
    }
}
↑非同期処理の完了後に別の処理を行う
マイケル
マイケル
引数としてコールバックを渡して、処理が完了したら呼び出す方式です。
実装自体は簡単ですが、複数の非同期処理が重なっていくと処理がネストし、可読性もよくないというデメリットがあります。
エレキベア
エレキベア
コールバック地獄に陥ってしまうクマね
マイケル
マイケル
同じ処理をUniTaskで実行すると下記のようになります。
using Cysharp.Threading.Tasks;
using UnityEngine;

namespace UniTaskTest
{
    public class UniTaskSample01 : MonoBehaviour
    {
        private async void Start()
        {
            await HelloWorldTask();
            
            // コールバックを続けて書ける
            Debug.Log("Finish!!");
        }
        
        private async UniTask HelloWorldTask()
        {
            await UniTask.Delay(1000);
            Debug.Log("Hello, World");
        }
    }
}
↑非同期処理の完了後に別の処理を行う(UniTask版)
マイケル
マイケル
非同期処理の実行後に続けて書けるため、より自然に感じるのではないでしょうか?
非同期処理を多用する場合にコンパクトに記述することができるのは、async/awaitを使う大きなメリットです。
マイケル
マイケル
また、UniTaskには戻り値を定義できる機能も備わっているというのも魅力の一つです。
エレキベア
エレキベア
これはコルーチンよりUniTaskの方が断然いいクマね
マイケル
マイケル
一点、コルーチンと比べた時のデメリットとして、オブジェクトのDestroy時に非同期処理が破棄されない点については注意が必要です。
こちらはキャンセルトークンという仕組みを使って実装しますが、この後みていきましょう!
エレキベア
エレキベア
まあMonoBehaviourの機能ではないから当然といえば当然クマね・・・

UniTaskの基本的な使い方

マイケル
マイケル
それではUniTaskの基本的な使い方について見ていきます!

導入方法

マイケル
マイケル
UniTaskの導入はGitHubから行います。
manifest.jsonに追記してもいいですし、UPM経由でインストールしてもOKです!
詳しくはGitHubのREADMEをご参照ください!

GitHub – Cysharp/UniTask

エレキベア
エレキベア
導入は一瞬クマね

シンプルな使い方

マイケル
マイケル
まず、最もシンプルな使い方についておさらいです。
先ほど紹介した通り、下記のようにasync/awaitで処理を実行することができます。
using Cysharp.Threading.Tasks;
using UnityEngine;

namespace UniTaskTest
{
    public class UniTaskSample01 : MonoBehaviour
    {
        private async void Start()
        {
            await HelloWorldTask();
            
            // コールバックも続けて書ける
            Debug.Log("Finish!!");
        }
        
        private async UniTask HelloWorldTask()
        {
            await UniTask.Delay(1000);
            Debug.Log("Hello, World");
        }
    }
}
↑シンプルな処理
マイケル
マイケル
そしてUniTask<型>と記述することで戻り値の型を指定することができます。
下記は処理終了後に文字列を返却する例です。
using Cysharp.Threading.Tasks;
using UnityEngine;

namespace UniTaskTest
{
    public class UniTaskSample02 : MonoBehaviour
    {
        private async void Start()
        {
            var message = await HelloWorldTask();
            Debug.Log(message);
        }
        
        private async UniTask<string> HelloWorldTask()
        {
            await UniTask.Delay(1000);
            Debug.Log("Hello, World");
            return "Finish!!";
        }
    }
}
↑戻り値も指定できる
エレキベア
エレキベア
非同期処理で戻り値が指定できるのは便利クマね

asyncメソッド以外から実行する

マイケル
マイケル
asyncメソッド以外から、非同期処理を呼び出すこともできます。
その場合、関数実行時にForget()を付けないと警告が出てしまうため注意しましょう。
using Cysharp.Threading.Tasks;
using UnityEngine;

namespace UniTaskTest
{
    public class UniTaskSample03 : MonoBehaviour
    {
        private void Start()
        {
            HelloWorldTask().Forget();
            
            // awaitしていないためこちらが先に呼ばれる
            Debug.Log("not Finish!!");
        }
        
        private async UniTask HelloWorldTask()
        {
            await UniTask.Delay(1000);
            Debug.Log("Hello, World");
        }
    }
}
↑async以外から呼び出す
マイケル
マイケル
また、もちろんですがawaitしていないためその後の処理は続けて実行されることにも注意しましょう。
エレキベア
エレキベア
awaitしないと止まらないクマね

複数の非同期処理を待機する

マイケル
マイケル
便利な機能として、WhenAllがあります。
下記のように実装することで複数の非同期処理の結果を待機することができます。
using Cysharp.Threading.Tasks;
using UnityEngine;

namespace UniTaskTest
{
    public class UniTaskSample04 : MonoBehaviour
    {
        private async void Start()
        {
            // 複数の非同期処理を待機して結果を受け取る
            var (msg1, msg2, msg3) 
                = await UniTask.WhenAll(GetMessage1(), GetMessage2(), GetMessage3());
            Debug.Log(msg1 + msg2 + msg3);
        }

        private async UniTask<string> GetMessage1()
        {
            await UniTask.Delay(1000);
            return "Message1";
        }
        
        private async UniTask<string> GetMessage2()
        {
            await UniTask.Delay(2000);
            return "Message2";
        }
        
        private async UniTask<string> GetMessage3()
        {
            await UniTask.Delay(3000);
            return "Message3";
        }
    }
}
↑複数の非同期処理の待機
エレキベア
エレキベア
これはロード処理なんかで使えそうクマね

非同期処理を中断する

マイケル
マイケル
最後に、非同期処理を中断する方法についてです。
下記のようにnew CancellationTokenSource()でキャンセル用のトークンを生成してawaitする非同期処理に渡してあげることで、Cancel()が実行されたタイミングで処理を中断することができます。
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;

namespace UniTaskTest
{
    public class UniTaskSample05 : MonoBehaviour
    {
        private CancellationTokenSource _cancellationTokenSource;
        
        private void Start()
        {
            _cancellationTokenSource = new CancellationTokenSource(); // キャンセルトークン生成
            
            LongWaitTask(_cancellationTokenSource.Token).Forget();
            
            Destroy(gameObject);
        }

        private void OnDestroy()
        {
            // Destroy時にキャンセルする
            _cancellationTokenSource?.Cancel();
        }

        private async UniTask LongWaitTask(CancellationToken token)
        {
            await UniTask.Delay(3000)
                .WithCancellation(token); // トークンを指定
            
            // キャンセルされるとその後の処理も実行されない
            Debug.Log("finish!!");
        }
    }
}
↑キャンセルトークンを使った中断処理
マイケル
マイケル
先ほど話した通り、コルーチンはオブジェクト破棄時に非同期処理も破棄されるのに対して、
UniTaskは自身で中断処理を定義しないといけない ため注意しましょう!
エレキベア
エレキベア
ここは中々面倒くさいクマね・・・
マイケル
マイケル
また、上記のようにOnDestroyでキャンセルさせる場合には、簡略化したGetCancellationTokenOnDestroyといった拡張メソッドもあるため、可能ならこちらを使用する方が楽そうですね・・・。
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;

namespace UniTaskTest
{
    public class UniTaskSample06 : MonoBehaviour
    {
        private void Start()
        {
            var token = this.GetCancellationTokenOnDestroy(); // キャンセルトークン生成
            
            LongWaitTask(token).Forget();
            
            Destroy(gameObject);
        }

        private async UniTask LongWaitTask(CancellationToken token)
        {
            await UniTask.Delay(3000)
                .WithCancellation(token); // トークンを指定
            
            // キャンセルされるとその後の処理も実行されない
            Debug.Log("finish!!");
        }
    }
}
↑OnDestroyでキャンセルする処理を簡略化した場合
エレキベア
エレキベア
これならまだ許せるクマ

UniTrackerでの状態監視

マイケル
マイケル
基本的な使用方法についての紹介は以上になりますが、
UniTaskの状態を可視化できるUniTrackerについても紹介しておきます。
マイケル
マイケル
こちらは「Window → UniTask Tracker」から開くことができ、
下記のように各タスクの状態を監視することができます。
20220526 01↑UniTrackerでの確認
エレキベア
エレキベア
状態が分かるのは便利クマね
マイケル
マイケル
実行中のTaskが残っているかどうかについても
これでチェックができそうだね。

おわりに

マイケル
マイケル
というわけで今回はUniTaskの紹介でした!
どうだったかな??
エレキベア
エレキベア
早くasync/awaitを使いたいクマ〜〜〜〜!!
マイケル
マイケル
より詳しく知りたい方は、専用の書籍も出ているようなので
こちらも読んでみるといいかもしれません!

UniRx/UniTask完全理解 より高度なUnity C#プログラミング

エレキベア
エレキベア
つよつよプログラマ・・・
マイケル
マイケル
それでは今日はこの辺で!
アデューー!!
エレキベア
エレキベア
クマ〜〜〜〜〜

【Unity】UniTaskとasync/awaitを使って非同期処理を実装する【脱コルーチン】〜完〜

Unity
スポンサーリンク
この記事をシェアしよう!
フォローお待ちしています!
都会のエレキベア

コメント