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)
Frame time: 16.7ms → Main thread bottleneck
✅ With Job System (Multithreaded)
Frame time drops to ~5ms — uses all CPU cores
Hands-on steps
Install the Burst package
Package Manager → search "Burst" → Install. Job System is built-in since Unity 2018+. Add using Unity.Jobs, Unity.Collections.
Write a Job struct with IJob
struct MyJob : IJob { public NativeArray
Allocate a NativeArray and Schedule
new NativeArray
Complete and Dispose
handle.Complete() in LateUpdate. Read results from the NativeArray. array.Dispose() to free unmanaged memory.
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.
Press Run to benchmark. (Simulated from Unity measurements, not a live profile)
Code example
BasicIJobParallelFor — 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
AdvancedJob 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