Unity Term Book
Advanced Techniques

Job System & Burst

Unity's C# Job System lets you write safe multithreaded code — pair it with Burst Compiler for native SIMD that runs heavy computation extremely fast.

Imagine...

The Main Thread is a single chef doing every job. The Job System is an industrial kitchen line: each worker thread does an independent task (chop, cook, fry) in parallel. The head chef only schedules and collects results. Burst Compiler swaps every kitchen knife for a CNC mill — it turns C# into SIMD assembly, 10–100× faster.

The concept in detail

C# Job System: Unity does not let other threads touch UnityEngine objects (Transform, MonoBehaviour) for thread safety. The Job System’s rule: data lives in NativeArray<T> (unmanaged memory, shared across threads). A job struct implements IJob, IJobParallelFor, or IJobParallelForTransform. Schedule → run on worker threads → Complete before reading results.

Burst Compiler is an LLVM compiler that takes IL and emits high-performance native code with SIMD (Single Instruction Multiple Data) — many floats at once (SSE2, AVX2, NEON). Add [BurstCompile] on the job struct. Burst does not support managed objects, exceptions, or reference types — value types and pointers only.

NativeArray<T> is unmanaged memory: no GC overhead, readable/writable from multiple threads (with constraints). You must Dispose() when done or you leak. NativeContainer safety checks in the Editor catch race conditions automatically.

Workflow: Schedule() enqueues the job; Complete() waits before you read results (blocking). Use dependency chains (pass a JobHandle into the next Schedule()) to chain jobs without completing early. ECS + DOTS is built entirely on Job System + Burst — that is why ECS performance is so high.

Threading model: single thread vs Job System

❌ Without Jobs (Single Thread)

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

✅ With Job System (Multithreaded)

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

Frame time drops to ~5ms — uses all CPU cores

Hands-on steps

1

Install the Burst package

Package Manager → search "Burst" → Install. Job System is built-in since Unity 2018+. Add using Unity.Jobs, Unity.Collections.

2

Write a Job struct with IJob

struct MyJob : IJob { public NativeArray data; public void Execute() { ... } } — no MonoBehaviour, no reference types.

3

Allocate a NativeArray and Schedule

new NativeArray(count, Allocator.TempJob) → new MyJob { data = array } → .Schedule() → keep the JobHandle.

4

Complete and Dispose

handle.Complete() in LateUpdate. Read results from the NativeArray. array.Dispose() to free unmanaged memory.

5

Add [BurstCompile]

Put [BurstCompile] on the struct — Jobs menu → Open Inspector to see disassembly. Burst shows green in the Profiler when active.

Interactive simulator

Compare compute speed: Main Thread vs Job System vs Job + Burst.

Main Thread
--
Job System
--
Job + Burst
--

Press Run to benchmark. (Simulated from Unity measurements, not a live profile)

Code example

Basic

IJobParallelFor — process N elements in parallel across threads.

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();
  }
}

Code example

Advanced

Job dependency chain — several jobs in sequence without Complete() in between.

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();
}

📌 Quick recap

  • A Job is a struct — no class/managed types
  • NativeArray: unmanaged, must Dispose() when done
  • [BurstCompile] → SIMD native code, 10–100× faster
  • Dependency chain: do not Complete() in the middle
  • Allocator.TempJob: max 4 frames; Persistent: long-lived

⚠️ Common mistakes

  • ❌ NativeArray not disposed warning

    Forgot Dispose() → native leak GC cannot reclaim

    ✅ Dispose in OnDestroy, or Allocator.TempJob with using

  • ❌ InvalidOperationException: The previously scheduled job ... writes...

    Reading a NativeArray from the main thread before Complete()

    ✅ Call handle.Complete() before touching the NativeArray on the main thread