この記事について

この記事は私がゲーム制作・学習等で学んだ知識を元に非同期処理の使い方をわかりやすく説明していきます。

この記事でわかること

  • 同期処理と非同期処理の違い
  • コルーチン(Coroutine)を使った非同期処理
  • async/await を使った非同期プログラミング
  • スレッド(Thread)を使った処理

Unityの同期処理・非同期処理とは?

同期処理(Synchronous)の特徴

同期処理とは、1 つの処理が終わるまで次の処理を待つ仕組みです。
UnityのUpdateなどで時間のかかる処理を続けると、その間は次のフレームへ進めません。

  • 処理が順番に実行されるので、デバッグが簡単
  • メインスレッドで重い処理を続けると、フレームが止まる原因になる

非同期処理(Asynchronous)の特徴

非同期処理とは、処理の完了を待つ間に実行権を返し、他の処理を進められる仕組みです。

  • 待機中にメインスレッドをブロックせず、他の処理を進められる
  • Web通信やファイル読み込みなどの完了を待つ処理に便利

非同期処理にしただけで、重い計算が自動的に別スレッドへ移るわけではありません。待機の仕方と、どのスレッドで処理するかは分けて考えましょう。メインスレッドに重い処理が残っていると、非同期処理を使っていても画面が止まることがあります。

Unityの同期・非同期処理を比べる概念図。メインスレッドをブロックする同期処理中は次のフレームへ進めない。非同期の待機では実行権を返して他の処理を進め、条件を満たすと再開する。コルーチンやasync指定だけで重い計算が別スレッドへ移るわけではない。
同期処理と非同期の待機を比較した概念図。帯の長さは実測時間を表していません。再開条件やスレッドはAPI・呼び出し元により異なります。 参考:Unity公式:非同期プログラミング/Unity公式:コルーチン/Microsoft公式:非同期処理のシナリオ

非同期処理の種類と役割?

種類主な用途メリット注意点 / 制約
コルーチン時間・フレーム単位の待機途中で中断し、後で再開できるメインスレッドで動く。重い計算には注意
async / await通信・読み込みなどの完了待ち完了後の処理を順番に書ける付けるだけでは別スレッドにならない
Thread数値計算などを別スレッドへ分離メイン処理と分けて実行できるUnity API・データ共有・終了処理に注意

コルーチン(Coroutine)を使った非同期処理

コルーチン(Coroutine)とは?

Unityで時間経過やフレーム単位の待機を扱う方法の1つが、コルーチン(Coroutine)です。
コルーチンを使えば、特定の処理を一時停止し、一定時間後に再開することができます。

コルーチンの中の処理は基本的にメインスレッドで動きます。重い処理をそのまま入れるだけでは軽くならないので、処理の分割や待機を使い分けてください。

コルーチン(Coroutine)の使い方については以下の記事を参考にしてください。

async/awaitを使った非同期処理

async/await とは?

async/awaitを使うと、非同期処理の完了を待ってから続きを書けます。

  • awaitで処理の完了を待つ流れを、順番に記述できる
  • I/O処理(Web API・ファイル読み込み)などの非同期APIと組み合わせて使える

※あまり投稿者が理解をしていないので使い方は省略します。

using System.Threading.Tasks;
using UnityEngine;

public class AsyncTest : MonoBehaviour
{
    async void Start()
    {
        Debug.Log("処理A開始");

        await Task.Delay(2000); // 約2秒待機(Time.timeScaleの影響を受けない)
        // await Task.Yield(); // 実行権を返す(1フレーム待機の指定ではない)

        Debug.Log("処理B再開");
    }
}

この例のTask.Delayは、Unityの時間スケールとは関係なく待機します。Task.Yieldは現在の実行コンテキストへ非同期に続きを回すためのもので、必ず次のフレームまで待つ命令ではありません。フレーム単位で待ちたい場合は、コルーチンのyield return nullなどを使います。

asyncメソッドも、最初の未完了のawaitに到達するまでは同期的に実行されます。Unityのメインスレッドから始めたこの例は、通常はメインスレッドへ戻って続きを実行します。Task.Runに渡した処理は別スレッドで実行されるため、その中で呼ぶUnity APIに注意してください。

上のコードは待機の基本形です。実際のゲームでは、シーン移動やオブジェクト破棄時のキャンセル、例外処理も必要に応じて追加しましょう。

なお、Unity 6のWebビルドではTask.Delayは非対応です。このサンプルをそのままWeb向けに使わず、公式の.NET API対応表を確認してください。Task.Runなどにもプラットフォームごとの制約があります。

Thread(スレッド)を使った非同期処理

Thread(スレッド)とは?

Thread(スレッド)を使うと、数値計算などの処理を別スレッドで実行できます。

  • 計算処理をメインスレッドから分離できる。ただし、処理負荷や待ち合わせによっては動作に影響する

※あまり投稿者が理解をしていないので使い方は省略します。

using System.Threading;
using UnityEngine;

public class ThreadTest : MonoBehaviour
{
    private Thread workerThread;

    void Start()
    {
        workerThread = new Thread(HeavyProcess);
        workerThread.Start();
    }

    void HeavyProcess()
    {
        Debug.Log("重い処理開始");

        // ここで時間のかかる処理を実行(例: 数値計算)
        for (int i = 0; i < 100000000; i++)
        {
            // ダミー計算
            float value = Mathf.Sqrt(i);
        }

        Debug.Log("重い処理終了");
    }
}

このコードはスレッドを開始する流れを示す学習用の例です。Mathf.Sqrtや標準のDebug.Logのように別スレッドから利用できるものもありますが、多くのUnity APIはメインスレッド専用です。HeavyProcessの中でGameObjectやTransform、UIなどを操作しないようにしましょう。

実際のゲームでは、終了・破棄時に処理を中断する仕組みや、結果をメインスレッドへ渡す仕組みも必要です。結果待ちのためにメインスレッドでThread.Joinを呼ぶと、その間は処理が止まります。また、C#のThreadはWebビルドなどで制約があるため、対象プラットフォームで使えるかを確認してください。

SceneManager.LoadSceneAsync()で非同期ロード

「SceneManager.LoadSceneAsync()」で非同期でシーンをロードし、進捗状況を表示可能になります。
SceneManagerについては以下のサイトを参考にしてください。

まとめ

・同期処理(Synchronous)は順番に実行され、重い処理はフレームを止める原因になる
・コルーチン(Coroutine)は時間経過やフレーム単位の待機に便利
・async/awaitは非同期処理の完了を待つ流れを書きやすくする
・非同期処理と別スレッドでの実行は区別して使い分けよう!

参考:Unity公式:async/awaitサポート/Microsoft:非同期プログラミングのシナリオ/Microsoft:Task.Yield