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 미사용 (싱글 스레드)
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로 디스어셈블리 확인. Burst가 활성이면 Profiler에 초록으로 표시.
인터랙티브 시뮬레이터
연산 속도 비교: 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();
}
}코드 예제
고급잡 의존성 체인——중간에 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()