Unity Term Book
고급 기법

Job System & Burst

Unity C# Job System은 안전한 멀티스레드 코드를 쓰게 하고, Burst Compiler와 함께면 SIMD 네이티브로 무거운 연산을 가속합니다. NativeArray·IJob·의존성 체인과 Schedule/Complete 기초를 배우는 고성능 가이드입니다.

상상해 보세요...

Main Thread는 모든 일을 혼자 하는 한 명의 셰프입니다. Job System은 산업용 주방 라인: 각 워커 스레드가 독립 작업(썰기, 조리, 튀김)을 병렬로 합니다. 헤드 셰프는 스케줄만 잡고 결과만 모읍니다. Burst Compiler는 모든 칼을 CNC 밀로 바꿉니다——C#을 SIMD 어셈블리로 바꿔 10–100배 빠르게 만듭니다.

개념 자세히

C# Job System: Unity는 스레드 안전을 위해 다른 스레드가 UnityEngine 오브젝트(Transform, MonoBehaviour)를 건드리지 못하게 합니다. Job System의 규칙: 데이터는 NativeArray<T> (언매니지드 메모리, 스레드 간 공유)에 둡니다. 잡 구조체는 IJob, IJobParallelFor, 또는 IJobParallelForTransform를 구현합니다. Schedule → 워커 스레드에서 실행 → 결과를 읽기 전 Complete.

Burst Compiler는 IL을 받아 SIMD(Single Instruction Multiple Data——한 번에 여러 float, SSE2/AVX2/NEON) 고성능 네이티브 코드를 내는 LLVM 컴파일러입니다. 잡 구조체에 [BurstCompile] 을 붙이세요. Burst는 매니지드 오브젝트, 예외, 참조 타입을 지원하지 않습니다——값 타입과 포인터만.

NativeArray<T>는 언매니지드 메모리입니다: GC 오버헤드 없음, 제약 하에 다중 스레드 읽기/쓰기. 끝나면 Dispose() 해야 하며 그렇지 않으면 누수입니다. Editor의 NativeContainer safety checks가 레이스 컨디션을 자동으로 잡습니다.

워크플로: Schedule()이 잡을 큐에 넣고; Complete()는 결과를 읽기 전 대기(블로킹). dependency chains(다음 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로 디스어셈블리 확인. Burst가 활성이면 Profiler에 초록으로 표시.

인터랙티브 시뮬레이터

연산 속도 비교: 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();
  }
}

코드 예제

고급

잡 의존성 체인——중간에 Complete() 없이 여러 잡을 순서대로.

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은 struct——class/매니지드 타입 없음
  • NativeArray: 언매니지드, 끝나면 Dispose() 필수
  • [BurstCompile] → SIMD 네이티브, 10–100배 빠름
  • 의존성 체인: 중간에 Complete()하지 않음
  • Allocator.TempJob: 최대 4프레임; Persistent: 장기

⚠️ 흔한 실수

  • ❌ NativeArray not disposed warning

    Dispose() 잊음 → GC가 회수 못하는 네이티브 누수

    ✅ OnDestroy에서 Dispose, 또는 Allocator.TempJob + using

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

    Complete() 전에 메인 스레드에서 NativeArray 읽기

    ✅ 메인 스레드에서 NativeArray를 만지기 전 handle.Complete()