Unity Job System & Burst
Unity의 Job System은 멀티스레드 연산을 안전하게 작성할 수 있는 공식 API입니다. 여기에 Burst Compiler를 결합하면 C# 코드를 LLVM 기반 네이티브 코드로 컴파일해 SIMD 수준의 성능을 얻을 수 있습니다.
왜 Job System인가?
Section titled “왜 Job System인가?”기존 Thread나 Task는 Unity의 주요 API(Transform, Physics 등)에 스레드 안전하게 접근할 수 없습니다. Job System은 다음을 보장합니다.
- 데이터 경쟁 방지 — 읽기/쓰기 선언을 컴파일 타임에 검사
- Worker Thread 풀 자동 관리 — 플랫폼별 코어 수에 맞게 자동 분배
- Burst 친화적 설계 —
NativeContainer기반 데이터 레이아웃
핵심 인터페이스
Section titled “핵심 인터페이스”| 인터페이스 | 용도 |
|---|---|
IJob | 단일 작업 단위 |
IJobParallelFor | 인덱스 기반 병렬 반복 |
IJobFor | 스케줄 옵션을 세밀하게 제어하는 병렬 반복 |
기본 사용법
Section titled “기본 사용법”IJobParallelFor — 대량 오브젝트 이동
Section titled “IJobParallelFor — 대량 오브젝트 이동”using Unity.Collections;using Unity.Jobs;using UnityEngine;
// Job 구조체 — 힙 할당 없이 스택에서 동작public struct MoveJob : IJobParallelFor{ public NativeArray<Vector3> positions; public float deltaTime; public float speed;
public void Execute(int index) { positions[index] += Vector3.forward * speed * deltaTime; }}
public class BulletSystem : MonoBehaviour{ NativeArray<Vector3> _positions; JobHandle _jobHandle;
void Start() { _positions = new NativeArray<Vector3>(10000, Allocator.Persistent); }
void Update() { var job = new MoveJob { positions = _positions, deltaTime = Time.deltaTime, speed = 20f, };
// innerloopBatchCount: 한 워커에 한 번에 할당할 인덱스 수 _jobHandle = job.Schedule(_positions.Length, 64); }
void LateUpdate() { // 메인 스레드에서 결과 사용 전 반드시 Complete 호출 _jobHandle.Complete();
for (int i = 0; i < _positions.Length; i++) transform.GetChild(i).position = _positions[i]; }
void OnDestroy() { _jobHandle.Complete(); _positions.Dispose(); }}NativeContainer
Section titled “NativeContainer”Job System은 관리형 배열 대신 NativeContainer를 사용합니다. 관리형 메모리에 접근하면 Burst가 최적화할 수 없고, 스레드 안전성도 보장되지 않기 때문입니다.
| 컨테이너 | 특징 |
|---|---|
NativeArray<T> | 고정 크기 배열, 가장 기본 |
NativeList<T> | 동적 크기 (Unity.Collections 패키지) |
NativeHashMap<K,V> | 해시맵 |
NativeQueue<T> | 선입선출 큐 |
Allocator 선택 기준
Section titled “Allocator 선택 기준”// Temp — 한 프레임 이내 사용 후 DisposeNativeArray<int> temp = new NativeArray<int>(100, Allocator.Temp);
// TempJob — Job 내부에서 사용, 4프레임 이내 DisposeNativeArray<int> tempJob = new NativeArray<int>(100, Allocator.TempJob);
// Persistent — 장기 보관, OnDestroy에서 반드시 DisposeNativeArray<int> persistent = new NativeArray<int>(100, Allocator.Persistent);Burst Compiler
Section titled “Burst Compiler”[BurstCompile] 어트리뷰트를 붙이면 해당 Job을 Burst가 LLVM IR로 컴파일합니다.
using Unity.Burst;using Unity.Collections;using Unity.Jobs;
[BurstCompile]public struct SumJob : IJob{ [ReadOnly] public NativeArray<float> values; [WriteOnly] public NativeArray<float> result;
public void Execute() { float sum = 0f; for (int i = 0; i < values.Length; i++) sum += values[i]; result[0] = sum; }}Burst 주요 제약사항
Section titled “Burst 주요 제약사항”| 제약 | 이유 |
|---|---|
관리형 객체 사용 불가 (class, string) | GC 힙 접근 불가 |
static 변수 읽기 제한 | 공유 상태 → 경쟁 조건 |
| 예외(Exception) 불가 | 네이티브 코드에서 CLR 예외 처리 불가 |
try/catch 불가 | 위와 동일 |
Debug.Log 제한 | UnityEngine.Debug는 관리형 |
Debug.Log 대신 Unity.Burst.Debug 또는 BurstDiscard 어트리뷰트를 사용합니다.
[BurstCompile]public struct DebugJob : IJob{ public void Execute() { // Burst 빌드 시 이 메서드 호출이 제거됨 LogMessage(); }
[BurstDiscard] static void LogMessage() => Debug.Log("디버그 전용");}성능 프로파일링
Section titled “성능 프로파일링”Unity Profiler에서 Jobs 탭을 열면 워커 스레드별 Job 실행 시간을 확인할 수 있습니다.
Profiler → Jobs 탭 → 워커별 타임라인 확인Burst Inspector (Jobs > Burst > Open Inspector) → 생성된 어셈블리 확인innerloopBatchCount 튜닝
Section titled “innerloopBatchCount 튜닝”// 너무 작으면 스케줄 오버헤드 증가, 너무 크면 병렬화 효율 감소// 일반적으로 32~128 사이에서 Profiler로 측정하며 조정_jobHandle = job.Schedule(count, 64);// ❌ Complete 전에 NativeArray 접근 → InvalidOperationException_jobHandle = job.Schedule(...);Debug.Log(_positions[0]); // 위험!
// ✅ Complete 후 접근_jobHandle.Complete();Debug.Log(_positions[0]);
// ❌ Dispose 누락 → 메모리 누수 + 에디터 경고// ✅ OnDestroy 또는 using 블록에서 반드시 Dispose