【Unity】【C#】Unityでasync/await、Task入門!非同期処理をスマートに書く

Unityでasync/await、Taskを使うための基礎的な内容をまとめました。

Unity2018.2
C#6

はじめに

この記事はUnityでasync/await、Taskを使うための基礎的な内容についてまとめています。
入門編なので、async/await、Taskの概念の理解ととりあえず使えるようになることを目的としています。

応用的、実用的な使い方は以下の記事にまとめていますので、必要に応じて参照してください。

light11.hatenadiary.com

async/await Taskとは?非同期処理の基礎知識

async/await、Taskは「非同期処理」を行うための仕組みです。

では非同期処理とは何でしょうか?
これには次の三つの意味があるようです。

  1. メインスレッドの処理を止めたくないから、ある独立した処理(IOとか)を別のスレッドに逃がす
  2. 1.と同じ理由で、重い処理をシングルスレッドで複数フレームに分けて行う(Unityのコルーチン)
  3. 計算を早く終わらせたいから同じ計算に複数のCPUコアを使う

このうち、async/awaitを使うと1.や2.がいい感じに書けます。
そして特にTaskを組み合わせると1.がやりやすくなります。
語弊を恐れずざっくり一言でまとめると、async/await、Taskを使うと重い処理を別のスレッドに逃がす処理が書きやすくなります。

この辺りの話は下記の記事がわかりやすかったです。

ufcpp.net

Taskを理解する

さてasync/awaitの使い方を理解するにはまずTaskというクラスを知っておく必要があります。
Taskを使うと別スレッドでの処理が簡単に書けます。

using UnityEngine;
using System.Threading;
using System.Threading.Tasks;

public class Example : MonoBehaviour {

    private void Start()
    {
        // 重い処理を別スレッドで走らせるタスクを作成
        var task = Task.Run(() => {
            // この辺りは別スレッドで行われる
            Thread.Sleep(1000);
            return true;
        })
        // Taskが終わった後の処理(別スレッドで行われる)
        .ContinueWith(x => {});
    }
}

このあたりの話は以下の記事がわかりやすいです。

ufcpp.net

async/awaitを理解する

async/awaitはTaskによるマルチスレッド処理を使いやすくするための構文です。
こんな感じで使います。

// メソッドにasync修飾子を付ける、戻り値はTaskに
private async Task ExampleAsync()
{
    Debug.Log("開始"); // これはメインスレッドで処理される

    // Taskにawait演算子を付けることで、Taskによる別スレッドでの処理を待つ
    await Task.Run(() => {
        Thread.Sleep(1000); // これは別スレッドで処理される
    });

    Debug.Log("終了"); // これはメインスレッドで処理される
}

まず、awaitを使うことでTaskの終了を待つことができます。

そしてこのawaitはasync修飾子を付けたメソッド内でのみ使えるという決まりがあるので、
メソッドにasync修飾子を付けています。
これによりこのメソッドは非同期メソッドとなります。

この非同期メソッドは、通常のメソッドから普通に呼び出せます。

using UnityEngine;
using System.Threading;
using System.Threading.Tasks;

public class Example : MonoBehaviour {

    private void Start()
    {
        // 普通のメソッドみたいに呼び出せばOK
        var _ = ExampleAsync();
    }
    
    private async Task ExampleAsync()
    {
        Debug.Log("開始");
        await Task.Run(() => {
            Thread.Sleep(1000);
        });
        Debug.Log("終了");
    }
}

これはasync/awaitの基本的な使い方です。
ExampleAsync()の戻り値を変数に入れている理由は次節で触れます。

2024/10追記:
上記のコードはエラーハンドリングを考えると void Startではなく async void Start にして、ExampleAsyncをawaitした方がいいです

async Taskとasync void

前節でvar _ = ExampleAsync();のように非同期メソッドの戻り値を変数に格納しました。
これは、これをやらないと下記のような警告が無駄に表示されるためです。

実は、この警告は非同期メソッドの戻り値をvoidにすることでも消せます。(※下記は悪い例です(後述))

using UnityEngine;
using System.Threading;
using System.Threading.Tasks;

public class Example : MonoBehaviour {

    private void Start()
    {
        // 警告は表示されなくなる
        ExampleAsync();
    }
    
    // 戻り値をvoidに
    private async void ExampleAsync()
    {
        await Task.Run(() => {
            Thread.Sleep(1000);
        });
    }
}

しかしこれはやってはいけません。
async void警察に怒られるようです。笑

世の中にはasync void 警察がいて、軽い気持ちでasync voidを残しておくとすごく叱責されるので注意してください。
Unity2017で始めるTask(async~await) #C# - Qiita

まあ要するに、asyncなメソッドをawaitするかどうかはあくまで使う側が決めることだということです。
だから非同期メソッドはawaitできるようにしておくべき、つまりTaskを返すべきです。
ただそれだと上述のような警告が出てしまうので、それは変数に入れることで消しておきましょうという話です。

2024/10追記:
UnityのStartメソッドなどを async void にしたり、eventに登録するメソッドをasync void にするのはアリです。

参考

ufcpp.net

ufcpp.net

https://qiita.com/Temarin/items/ff74d39ae1cfed89d1c5qiita.com

qiita.com