ジョブを作成し、正しく実行するには、以下を行う必要があります。
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
を呼び出さないようにすると効率的です。
重要度の低いジョブを、より重要な仕事と競合しないフレームの一部にスケジュールします。
例えば、あるフレームの終わりと次のフレームの始まりの間にジョブが実行されていない間隔があり、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);