🧑‍💻

【Unity】非同期処理:コルーチン、Async Await、Task.Run

2024/11/23に公開

Unityでは、非同期処理を効果的に使うことで、
ロード時間のコントロールとや、画面のフリーズを防ぐことができます。
Unityでよく使われる非同期処理についてまとめています。


  1. コルーチン
  2. Async Await
  3. Task.Run
  4. まとめ

1 コルーチン

Unityの独自機能で、メインスレッド上で動作する非同期処理です。
IEnumerator を使用してフレーム単位で処理を進めることができます。
メインスレッド上で動作するため、UnityのAPIを使用できます。
反面、計算処理中にメインスレッドがブロックされるため、
ゲーム全体が「カクつく」ような動作になってしまうため、重たい処理には注意が必要です。

使用例

void Start()
{
    StartCoroutine(LoadGameAssets());
}

IEnumerator LoadGameAssets()
{
    yield return new WaitForSeconds(1); // ダミー待機
    Debug.Log("リソースロード完了");
}

2 Async Await

async/await は、C# 標準の非同期処理機能です。
非同期タスクを記述でき、メインスレッドで動作しながら処理を中断・再開できます。
UnityのStartUpdateasyncを付けることで利用可能です。
非同期処理が終了するとメインスレッドに復帰するため、
そのままUnity APIを使用することも可能です。

コルーチンと似たような挙動になりますが、フレーム単位の制御や、
Unity特有のタイマー処理を扱うならコルーチンの方が便利です。

使用例

async void Start()
{
    Debug.Log("非同期処理開始");
    // 非同期タスク(例:大量のデータの操作)を待機
    string result = await LoadDataAsync();
    Debug.Log("非同期処理終了");
}

void Update()
{
    Debug.Log("Call Update!");
}


async Task<string> LoadDataAsync()
{
    await Task.Delay(10); // バックグラウンド処理
    transform.position = new Vector3(0, 1, 0); // Unity APIの操作
    return "取得成功";
}

3 Task Run

Task.Run を使用すると、バックグラウンドスレッド処理を実行し、
Unityのメインスレッドを解放できます。
ただし、UnityのAPIは複数のスレッドが同時に同じリソースにアクセスしても、
データの整合性が保たれる状態(スレッドセーフ)ではないため、
バックグラウンドスレッドから直接Unity APIを呼び出すことはできません。

使用例

async void Start()
{
    Debug.Log("非同期処理開始");

    // 重い処理をバックグラウンドスレッドで実行
    int result = await PerformHeavyCalculation();

    Debug.Log($"非同期処理終了: 計算結果 = {result}");
    transform.position = new Vector3(0, 1, 0); // Unity APIの操作
}

void Update()
{
    Debug.Log("Call Update!");
}

async Task<int> PerformHeavyCalculation()
{
    return await Task.Run(() =>
    {
        Debug.Log("バックグラウンドスレッド: 重い計算処理開始");

        int sum = 0;
        for (int i = 0; i < 1000000000; i++)
        {
            sum += i;
        }
        Debug.Log("バックグラウンドスレッド: 計算処理完了");
        return sum;
    });
}

UnityのAPIを使用するには、メインスレッドに処理を戻す必要があります。

// メインスレッドの SynchronizationContext を保持
private SynchronizationContext _mainThreadContext;

void Start()
{
    // 現在の SynchronizationContext(メインスレッド)を取得
    _mainThreadContext = SynchronizationContext.Current;

    Debug.Log("非同期処理開始");

    // Task.Run を使用して非同期処理を開始
    StartBackgroundTask();
}

void Update()
{
    Debug.Log("Call Update!");
}

async void StartBackgroundTask()
{ 
    Debug.Log("メインスレッド: 処理開始");

    // バックグラウンドスレッドで重い計算処理を実行
    int result = await Task.Run(() =>
    {
        Debug.Log("バックグラウンドスレッド: 重い計算処理開始");
        int sum = 0;
        for (int i = 0; i < 1000000000; i++)
        {
            sum += i;
        }
        Debug.Log("バックグラウンドスレッド: 計算処理完了");
        return sum;
    });

    // メインスレッドに戻して Unity API を操作
    _mainThreadContext.Post(_ =>
    {
        Debug.Log($"メインスレッド: 計算結果は {result}");
        transform.position = new Vector3(0, 1, 0); // Unity API を操作
    }, null);

    Debug.Log("非同期処理終了");
}

まとめ

コルーチンが適しているケース

  • フレーム単位で制御する必要がある処理。
  • 軽量なタイマー処理やアニメーション制御。
  • Unity APIを頻繁に使用する非同期処理。

Async Awaitが適しているケース

  • 非同期I/O処理(ファイル読み書きやネットワーク通信)。
  • 複雑なタスクを簡潔に記述したい場合。

Task.Runが適しているケース

  • 重い計算処理をバックグラウンドで実行したい場合。
  • Unity APIを使用しないデータ変換や計算処理。

Discussion