Version: 2023.2
言語: 日本語
カスタム NativeContainer の例
ジョブの依存関係

ジョブの作成と実行

ジョブを作成し、正しく実行するには、以下を行う必要があります。

  • ジョブを作る: IJob インターフェースを実装します。
  • ジョブをスケジュールする: ジョブ上で Schedule メソッドを呼び出します。
  • ジョブの完了を待つ: ジョブがすでに完了している場合は、即座に戻ります。データにアクセスしたい場合は、ジョブ上で Complete メソッドを呼び出します。

ジョブの作成

Unityでジョブを作成するには、IJob インターフェースを実装する必要があります。IJob を使用すると、実行中の他のジョブと並列処理する 1 つのジョブをスケジュールすることができます。

IJobには、1 つの必須メソッド Execute があります。これは、ワーカースレッド がジョブを実行するたびに呼び出されます。

ジョブを作成する際に、他のメソッドがこのジョブを参照するために必要な JobHandle を作成することもできます。

重要: ジョブ内からの読み取り専用以外または可変の静的データへのアクセスに対する保護はありません。この種のデータへのアクセスはすべての安全システムを回避し、アプリケーションや Unity エディターをクラッシュさせる可能性があります。

Unity が実行されると、ジョブシステムはスケジュールされたジョブデータのコピーを作成し、複数のスレッドが同じデータを読み取り/書き込みできないようにします。ジョブが終了した後は、NativeContainer に書き込まれたデータだけにアクセスできます。これは、ジョブが使用する NativeContainer のコピーと元の NativeContainer オブジェクトの両方が同じメモリを指すためです。詳細は、スレッドセーフ型 に関するドキュメントを参照してください。

ジョブシステムがジョブキューからジョブをピックアップするとき、1 つのスレッド上で Execute メソッドを 1 度だけ実行します。通常、ジョブシステムはバックグラウンドスレッドでジョブを実行しますが、メインスレッドがアイドルの場合はメインスレッドを選択することができます。このため、ジョブは 1 フレーム以内に完了するように設計する必要があります。

ジョブのスケジュール

ジョブをスケジュールするには、Schedule を呼び出します。これにより、ジョブがジョブキューに入れられ、ジョブシステムは、依存関係 がある場合はその依存関係がすべて完了した時点でジョブの実行を開始します。いったんスケジュールされると、ジョブを中断することはできません。メインスレッドからのみ Schedule を呼び出すことができます。

ヒント: ジョブには Run メソッドがあります。Schedule の代わりにこれを使用すると、メインスレッドでジョブをすぐに実行できます。これはデバッグの目的で使うことができます。

ジョブの完了

Schedule を呼び出し、ジョブシステムがジョブを実行したら、JobHandle 状で Complete メソッドを呼び出し、ジョブのデータにアクセスできます。Complete メソッドを呼び出すのは、コードのできるだけ最後のほうにするのが効率的です。Complete を呼び出すと、メインスレッドは安全に NativeContainer インスタンスにアクセスできるようになります。Completeを呼び出すと、安全システムの状態もクリーンアップされます。これを行わないとメモリリークが発生します。

ジョブの例

以下は、2つの浮動小数点値を加算するジョブの例です。これは IJob を実装し、NativeArray を使用してジョブの結果を取得し、その中のジョブの実装により、Execute メソッドを使用します。

using UnityEngine;
using Unity.Collections;
using Unity.Jobs;

// Job adding two floating point values together
public struct MyJob : IJob
{
    public float a;
    public float b;
    public NativeArray<float> result;

    public void Execute()
    {
        result[0] = a + b;
    }
}

以下の例は、MyJob ジョブに基づいて、メインスレッドにジョブをスケジュールします。

using UnityEngine;
using Unity.Collections;
using Unity.Jobs;

public class MyScheduledJob : MonoBehaviour
{
    // Create a native array of a single float to store the result. Using a
    // NativeArray is the only way you can get the results of the job, whether
    // you're getting one value or an array of values.
    NativeArray<float> result;
    // Create a JobHandle for the job
    JobHandle handle;

    // Set up the job
    public struct MyJob : IJob
    {
        public float a;
        public float b;
        public NativeArray<float> result;

        public void Execute()
        {
            result[0] = a + b;
        }
    }

    // Update is called once per frame
    void Update()
    {
        // Set up the job data
        result = new NativeArray<float>(1, Allocator.TempJob);

        MyJob jobData = new MyJob
        {
            a = 10,
            b = 10,
            result = result
        };

        // Schedule the job
        handle = jobData.Schedule();
    }

    private void LateUpdate()
    {
        // Sometime later in the frame, wait for the job to complete before accessing the results.
        handle.Complete();

        // All copies of the NativeArray point to the same memory, you can access the result in "your" copy of the NativeArray
        // float aPlusB = result[0];

        // Free the memory allocated by the result array
        result.Dispose();
    }


}

Schedule と Completeのベストプラクティス

必要なデータが手に入ったらすぐにジョブ上で Schedule をコールし、結果が必要になるまで Complete を呼び出さないようにすると効率的です。

重要度の低いジョブを、より重要な仕事と競合しないフレームの一部にスケジュールします。

例えば、あるフレームの終わりと次のフレームの始まりの間にジョブが実行されていない間隔があり、1 フレームの遅延が許容できる場合、フレームの終わりにジョブをスケジュールし、その結果を次のフレームで使用することができます。あるいは、アプリケーションが他のジョブでその切り替わりの間隔を飽和させ、フレーム内のどこかに十分に利用されていない間隔がある場合、代わりにそこにジョブをスケジュールする方が効率的です。

プロファイラー を使って、Unity がどこでジョブの完了を待っているかを確認することもできます。メインスレッドの WaitForJobGroupID マーカーはこれを示します。このマーカーは、解決すべきデータ依存関係がどこかに存在していることを意味する場合があります。JobHandle.Complete を見て、メインスレッドを待たせているデータの依存関係がどこにあるかを突き止めることができます。

長時間続くジョブの使用を避ける

スレッドとは異なり、ジョブは実行を放棄しません。ジョブが開始されると、そのジョブワーカースレッドは他のジョブを実行する前にジョブの完了をコミットします。そのため、システム内の他のジョブに比べて完了するのに時間がかかるジョブを投入する代わりに、長く実行が続くジョブを 互いに依存する 小さなジョブに分割すると効率的です。

ジョブシステムは通常、ジョブの依存関係の複数のチェーンを実行します。そのため、長く実行されるタスクを複数に分割すると、複数のジョブチェーンが進行する可能性があります。その代わりに、ジョブシステムが長時間実行のジョブで埋め尽くされている場合、ワーカースレッドを完全に消費し、独立したジョブの実行をブロックしてしまうかもしれません。その結果、メインスレッドが明示的に待機している重要なジョブの完了時間が押し出され、メインスレッド上で本来は存在しないはずのストールが発生する可能性があります。

特に、長く実行が続く IJobParallelFor ジョブはジョブシステムに悪影響を与えます。なぜなら、これらのジョブタイプは意図的にジョブのバッチサイズに対して可能な限り多くのワーカースレッドで実行しようとするからです。長い並列ジョブを分割できない場合は、ジョブのスケジュール時にバッチサイズを大きくして、長く実行されるジョブをピックアップするワーカーの数を制限することを検討してください。

MyParallelJob jobData = new MyParallelJob();
jobData.Data = someData;  
jobData.Result = someArray;  

// Use half the available worker threads, clamped to a minimum of 1 worker thread
const int numBatches = Math.Max(1, JobsUtility.JobWorkerCount / 2); 
const int totalItems = someArray.Length;
const int batchSize = totalItems / numBatches;

// Schedule the job with one Execute per index in the results array and batchSize items per processing batch
JobHandle handle = jobData.Schedule(result.Length, totalItems, batchSize);

その他の参考資料

カスタム NativeContainer の例
ジョブの依存関係