Job System & Burst
Unity の C# Job System は安全なマルチスレッド処理を書けます。Burst Compiler と組み合わせるとネイティブ SIMD で重い計算を極めて高速に実行でき、DOTS の土台にもなります。図解とシミュレーターで Main Thread との差を体感しながら実践的に学べます。
想像してみてください...
Main Thread は一人のシェフが全仕事をこなす状態です。Job System は産業用キッチンライン: 各ワーカースレッドが独立タスク(切る、焼く、揚げる)を並列実行します。ヘッドシェフはスケジュールと結果収集だけ。Burst Compiler は包丁を CNC フライスに替え——C# を SIMD アセンブリにし、10–100 倍速にします。
概念の詳細
C# Job System: Unity はスレッド安全性のため、他スレッドから UnityEngine オブジェクト(Transform、MonoBehaviour)への触れを許しません。Job System のルール: データは
NativeArray<T>
(アンマネージドメモリ、スレッド間共有)に置く。Job 構造体は
IJob、
IJobParallelFor、または
IJobParallelForTransform を実装。Schedule → ワーカースレッドで実行 → 結果を読む前に Complete。
Burst Compiler は IL を受け取り、SIMD(Single Instruction Multiple Data)付き高性能ネイティブコードを出す LLVM コンパイラです——多数の float を一度に(SSE2、AVX2、NEON)。Job 構造体に
[BurstCompile]
を付けます。Burst はマネージドオブジェクト、例外、参照型をサポートしません——値型とポインタのみ。
NativeArray<T> はアンマネージドメモリ: GC オーバーヘッドなし、制約付きで複数スレッドから読み書き可能。使い終わったら
Dispose()
しないとリークします。Editor の NativeContainer safety checks がレース条件を自動検出します。
流れ: Schedule() が Job をキューイング、Complete() が結果読み取り前に待機(ブロッキング)。dependency chain(次の Schedule() に JobHandle を渡す)で早期 Complete なしに連鎖できます。ECS + DOTS は Job System + Burst の上に構築——だから ECS の性能が高いのです。
スレッドモデル: シングルスレッド vs Job System
❌ Jobs なし(シングルスレッド)
Frame time: 16.7ms → Main thread bottleneck
✅ Job System あり(マルチスレッド)
フレーム時間が約 5ms に短縮 — 全 CPU コアを活用
ハンズオン手順
Burst パッケージをインストール
Package Manager → "Burst" を検索 → Install。Job System は Unity 2018+ で標準。using Unity.Jobs、Unity.Collections を追加。
IJob 付き Job 構造体を書く
struct MyJob : IJob { public NativeArray
NativeArray を確保して Schedule
new NativeArray
Complete と Dispose
LateUpdate で handle.Complete()。NativeArray から結果を読む。array.Dispose() でアンマネージドメモリを解放。
[BurstCompile] を付ける
構造体に [BurstCompile] —— Jobs メニュー → Open Inspector でディスアセンブリ確認。Profiler で Burst が緑なら有効。
インタラクティブシミュレーター
計算速度を比較: Main Thread vs Job System vs Job + Burst。
Run を押してベンチマーク。(Unity 実測に基づくシミュレーションであり、実測ではありません)
コード例
基本IJobParallelFor——N 要素をスレッド間で並列処理。
using Unity.Jobs;
using Unity.Collections;
using Unity.Burst;
using Unity.Mathematics;
using UnityEngine;
// [BurstCompile] → compile to SIMD native code
[BurstCompile]
struct CalculatePositionsJob : IJobParallelFor
{
[ReadOnly] public NativeArray<float3> velocities;
public NativeArray<float3> positions; // Write results here
public float deltaTime;
// Execute runs in parallel for each index
public void Execute(int index)
{
positions[index] += velocities[index] * deltaTime;
}
}
public class FlockSimulation : MonoBehaviour
{
const int COUNT = 10000;
NativeArray<float3> positions, velocities;
JobHandle jobHandle;
void Awake()
{
positions = new NativeArray<float3>(COUNT, Allocator.Persistent);
velocities = new NativeArray<float3>(COUNT, Allocator.Persistent);
}
void Update()
{
jobHandle.Complete(); // Wait for last frame's job
jobHandle = new CalculatePositionsJob
{
velocities = velocities,
positions = positions,
deltaTime = Time.deltaTime
}.Schedule(COUNT, 64); // 64 = batch size per thread
}
void OnDestroy()
{
jobHandle.Complete();
positions.Dispose();
velocities.Dispose();
}
}コード例
上級Job 依存チェーン——途中で Complete() せず複数 Job を直列実行。
using Unity.Jobs;
using Unity.Collections;
using Unity.Burst;
// Two jobs in sequence via a dependency — main thread is not blocked in between
void ScheduleChain(NativeArray<float> data)
{
// Job 1: square each value
var squareJob = new SquareJob { data = data };
JobHandle squareHandle = squareJob.Schedule(data.Length, 32);
// Job 2: needs job 1 first → pass the handle as a dependency
var sumJob = new SumJob { data = data };
JobHandle sumHandle = sumJob.Schedule(squareHandle); // dependency!
// Main thread is not blocked. The chain runs in parallel with rendering...
// ...until LateUpdate needs the result:
sumHandle.Complete();
Debug.Log($"Sum of squares: {sumJob.result[0]}");
}
// Several jobs in parallel, then merge:
void ScheduleParallel()
{
var jobA = new JobA().Schedule();
var jobB = new JobB().Schedule();
// CombineDependencies: wait for BOTH before jobC
var combined = JobHandle.CombineDependencies(jobA, jobB);
var jobC = new JobC().Schedule(combined);
jobC.Complete();
}📌 要点
- ▸Job は構造体——class/マネージド型は不可
- ▸NativeArray: アンマネージド、使い終わったら Dispose()
- ▸[BurstCompile] → SIMD ネイティブコード、10–100 倍速
- ▸依存チェーン: 途中で Complete() しない
- ▸Allocator.TempJob: 最大 4 フレーム、Persistent: 長期
⚠️ よくあるミス
❌ NativeArray not disposed 警告
Dispose() 忘れ → GC が回収できないネイティブリーク
✅ OnDestroy で Dispose、または Allocator.TempJob と using
❌ InvalidOperationException: The previously scheduled job ... writes...
Complete() 前に Main Thread から NativeArray を読んだ
✅ Main Thread で NativeArray に触る前に handle.Complete()