Unity Term Book
高度な技術

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 構造体は IJobIJobParallelFor、または 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 なし(シングルスレッド)

Main
Physics → AI → Render → Input → ...
Worker 1
idle idle idle idle...
Worker 2
idle idle idle idle...
Worker 3
idle idle idle idle...

Frame time: 16.7ms → Main thread bottleneck

✅ Job System あり(マルチスレッド)

Main
Input+Render
Complete+Apply
Worker 1
Physics Batch Job
Worker 2
AI Pathfinding Job
Worker 3
Procedural Mesh Job

フレーム時間が約 5ms に短縮 — 全 CPU コアを活用

ハンズオン手順

1

Burst パッケージをインストール

Package Manager → "Burst" を検索 → Install。Job System は Unity 2018+ で標準。using Unity.Jobs、Unity.Collections を追加。

2

IJob 付き Job 構造体を書く

struct MyJob : IJob { public NativeArray data; public void Execute() { ... } } —— MonoBehaviour なし、参照型なし。

3

NativeArray を確保して Schedule

new NativeArray(count, Allocator.TempJob) → new MyJob { data = array } → .Schedule() → JobHandle を保持。

4

Complete と Dispose

LateUpdate で handle.Complete()。NativeArray から結果を読む。array.Dispose() でアンマネージドメモリを解放。

5

[BurstCompile] を付ける

構造体に [BurstCompile] —— Jobs メニュー → Open Inspector でディスアセンブリ確認。Profiler で Burst が緑なら有効。

インタラクティブシミュレーター

計算速度を比較: Main Thread vs Job System vs Job + Burst。

Main Thread
--
Job System
--
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()