ユーセンブログ

ゲーム開発に関することをたまに書きます

【Unity】async/awaitをコルーチン的に(簡単に)使いたい

はじめに

async/await使えば処理を早くしやすくなるし、コルーチンっぽく使えれば便利なのではということでサクッと非同期的な処理をするときどうするか調べてみました。 webからデータを読み込んで~みたいなのはたくさんあったのでとにかく重たい関数、処理を非同期にぶん投げるにはどうするかを中心に書いて行きます。

なぜ、コルーチンではなくasync/awaitなのか

理由としては

  • コルーチンだと戻り値がないので欲しい時面倒くさい。
  • 別スレッドに投げているわけではないので厳密にいう並列処理にはできない。
  • いちいちStartCoroutineで呼ぶのがダルい。関数呼び出し一発でいいじゃん。
  • UnityがC#7になったし、そろそろ使って行きたい。

から重要なところだけでなく、細かくasync/awaitを使っていきたいと思ってます。

使っていく

asyncな関数

基本的な関数の宣言方法としては、asyncをつけた関数の中で何らかのTaskをawaitで待たせることで単一の関数の中で非同期に処理を終わるのを待たせることができます。簡単な例をいくつか紹介します。

  • 並列処理の例
  • 数秒間待ってから処理
  • 定期的に処理を行う
並列処理

Taskの中身が別のスレッドで実行されている。

 using System.Threading.Tasks;
    private async Task  AsyncFunc()
    {
        //awaitをTask.Runにつけると処理を待ってくれる
        await Task.Run(() =>
        {
            hogehoge();
        });
        fugafuga();
    }
一定時間処理を待ってから実行

Task.Delayで一定時間待ってから処理をする。別スレッドから帰ってくるのを待っているため、別の処理は待っている間動き続ける。

 using System.Threading.Tasks;
    private async Task AsyncFunc()
    {
        await Task.Delay(1000);//1000ミリ秒(一秒)待って
        hogehoge();//処理を実行
    }
定期的に処理を行う

ループを自分で作って一定時間を待って定期的に処理を回す。

 using System.Threading.Tasks;
    async Task AsyncFunc()
    {
     //だいたい1フレに一回ぐらいのペースで実行される
        while(true)
        {
            await Task.Delay(16);
            hogehoge();
            //何かしらの条件で抜ける
            if ("ループ脱出条件") break;
        }
    }

戻り値にTaskがあるのにretrun文がないぞ?となると思うのですが、asyncな関数は戻り値のTaskをvoidとして扱うことができます。なので実は戻り値はvoidでも動きます。しかし、関数を呼び出す側がタスクの状態を見る術がなくなるのでTaskで返してあげると親切です。

また、注意事項として、並列処理の例で上げたTask.Runの中では、Unityのオブジェクト類の操作(transformの操作など)はできません。基本的にはファイル操作やデータのエンコード、デコードなどに使用します。

呼び出し方

呼び出し方は戻り値がvoidなものであればシンプルにasyncな関数を普通に呼び出せば実行できます。 そのまま呼び出すと、呼び出す際に終わるの待たないで勝手に処理するぞ的な警告が出ますが、無視してもちゃんと非同期で動きます。気になる方は帰って来るTaskを変数で受け取っておけば警告は消えます。

f:id:k_tachiban:20190411194024p:plain
呼び出し時にはこんな感じの警告が出ますが無視してもOK

 using System.Threading.Tasks;
    public void main()
    {
         //これだけでもOK
         AsyncFunc();

        //お行儀良く書くなら呼び出し方は基本的にこっち
        //Taskを受けとっておけばあとでコケたときとかのエラー処理とかできる。警告も消える。
        //戻り値がほしいときは引数のTaskを後で使う
        Task task = AsyncFunc();
    }

戻り値つきのasyncな関数

戻り値付きのasyncな関数を使う場合は戻り値をTaskの形で宣言し、return Tの形で返します。 関数は基本的にはこれだけですが、呼び出し側に少し注意が必要になります。

    async Task<string> ReturnAsyncFunc()
    {
        await Task.Delay(1000);
        return "hogehoge";
    }

戻り値付きのasync関数を使う場合は、呼び出し側の関数もasyncにする必要があります。 理由としては、 この場合asyncをつけないと、Task.ResultがReturnAsyncFunc()の完了を待つため、main関数の中で処理が止まり、結果としてReturnAsyncFunc()のTask.Delay(1000);の影響で1000ミリ秒がメインスレッドの処理が止まってしまいます(全体の処理が止まる)。 そのため、呼び出し側の関数にasyncをつけてawait task;で非同期に完了を待ち、結果を受け取る必要があります。

呼び出し方

    public async Task main()
    {
        //戻り値が必要な場合は一度Taskの変数を持って
        Task<string> task =  ReturnAsyncFunc();
        //awaitでタスクの終了を非同期で待ち
        await task;
        //resultで受け取る。
        string hoge =   task.Result;

        //短縮したい場合はこうでも行ける
        hoge = await ReturnAsyncFunc();
    }
//この書き方だと処理が止まる(main関数にasync/awaitなし)
    public Task main()
    {
        Task<string> task =  ReturnAsyncFunc();
        string hoge =   task.Result;
    }

感想

  • コルーチンでよく使う処理の一部はこれで結構簡略化できそう
  • 呼び出しタイミングをきちんと制御した。スレッドをしばき倒したい。などの場合はUniTaskがあると良さそう
  • コルーチンのほうがいい場合もおそらくあるのでそこはケースバイケース

    参考文献

    qiita.com

neue.cc