-
Unity Job System2023년 10월 30일
- 유니얼
-
작성자
-
2023.10.30.:31
728x90Unity Job System 소개
Unity Job System은 Unity에서 제공하는 멀티스레딩 프레임워크로, CPU 리소스를 효율적으로 활용하여 게임 성능을 개선합니다. 이 시스템은 Unity의 내부 기능과 통합되어 있으며, 개발자가 작성한 코드와 Unity는 동일한 워커 스레드를 공유하여 성능 향상을 극대화합니다.
https://docs.unity3d.com/kr/2018.4/Manual/JobSystem.html
멀티쓰레딩이란?
멀티스레딩(Multithreading)은 컴퓨터 프로그램이 동시에 여러 개의 실행 스레드를 가지고 작업을 수행하는 프로그래밍 및 실행 방식입니다. 각 스레드는 프로그램 내에서 독립적으로 실행되며, 프로세서의 여러 코어에서 병렬로 처리됩니다. 멀티스레딩은 컴퓨팅 시스템의 성능을 향상하고 병렬화 작업을 가능하게 하여 다양한 응용 프로그램에서 사용됩니다.
단일 스레드 컴퓨팅 시스템에서는 한 번에 하나의 명령어가 입력되고 한 번에 하나의 결과가 출력됩니다. 프로그램을 로드하고 완료하는 데 걸리는 시간은 CPU가 수행해야 하는 작업량에 따라 다릅니다.
멀티스레딩은 여러 코어에서 한 번에 여러 개의 스레드를 처리하는 CPU 성능을 활용하는 프로그래밍의 한 유형입니다. 한 번에 하나가 아니라, 동시에 여러 개의 작업 또는 명령을 실행합니다.
어떤 스레드는 기본적으로 프로그램이 시작할 때 실행됩니다. 이 스레드가 바로 ’ 메인스레드’입니다. 메인 스레드는 작업을 처리하기 위해 새로운 스레드를 생성합니다. 이러한 새 스레드는 다른 스레드와 병렬로 실행되며, 대개 실행이 완료되면 메인 스레드와 결과를 동기화합니다.
이러한 멀티스레딩 방식은 여러 개의 작업이 오랫동안 실행되는 경우에 적합합니다. 하지만 일반적으로 게임 개발 코드에는 한 번에 실행해야 할 작은 명령이 많이 들어 있습니다. 각 명령에 대해 스레드를 만들면 그 수가 너무 많아지고 각각의 수명도 짧아집니다. 따라서 CPU 및 운영체제의 프로세싱 능력을 초과할 수 있습니다.
스레드 풀을 사용하면 스레드 수명 주기 문제를 완화할 수 있습니다. 하지만 스레드 풀을 사용해도 동시에 활성화된 스레드 수가 너무 많을 수 있습니다. CPU 코어보다 스레드 수가 더 많으면 CPU 리소스를 놓고 스레드 간에 경쟁이 벌어지고, 이로 인해 컨텍스트 스위칭이 빈번하게 발생합니다. 컨텍스트 스위칭은 실행 도중에 스레드 상태를 저장하고 다른 스레드에 대한 작업을 진행한 후 첫 번째 스레드를 재구성하여 나중에 계속 처리하는 프로세스입니다. 컨텍스트 스위칭은 리소스를 매우 많이 소모하므로 가급적 사용하지 않는 것이 좋습니다.
기존 유니티 엔진
유니티에서는 기본적으로 메인 쓰레드를 기반으로 엔진이 동작합니다. 그리고 디바이스에 CPU 논리 코어당 하나의 작업 스레드(Worker Thread)를 생성하여 백그라운드 작업(데이터 로딩, 리소스 처리, 물리 시뮬레이션, 렌더링 준비와 같은 비동기 작업)을 수행하고 마지막으로 렌더 쓰레드를 통해 유저의 디바이스에 게임이 보여지게 됩니다.
메인 스레드 (Main Thread):
- Unity의 주요 스레드로, 게임의 주요 논리와 사용자 인터페이스 업데이트를 담당합니다.
- 사용자 입력 처리, 게임 객체의 업데이트, 렌더링, 물리 시뮬레이션 등 핵심 엔진 작업을 처리합니다.
- 주로 싱글 스레드에서 실행되며, 프레임마다 주요 작업을 수행합니다.
작업 스레드 (Worker Threads):
- Unity는 메인 스레드 외에도 작업 스레드 풀을 사용하여 추가 스레드를 활용합니다.
- 작업 스레드는 메인 스레드에서 생성되며, 주로 백그라운드 작업을 수행합니다.
- 작업 스레드는 데이터 로딩, 리소스 처리, 물리 시뮬레이션, 렌더링 준비와 같은 비동기 작업을 처리합니다.
렌더링 스레드 (Rendering Thread):
- Unity는 렌더링 스레드를 사용하여 그래픽 렌더링 작업을 처리합니다.
- 렌더링 스레드는 메인 스레드와 별도로 실행되며, 화면에 그래픽을 그립니다.
- 이렇게 함으로써 CPU와 GPU 작업을 병렬로 수행하여 성능을 향상시킵니다.
Job System의 원리
Job System은 멀티 쓰레드 방식의 프로그래밍 기법이지만 쓰레드를 대신하여 잡을 만들어 멀티스레드 코드를 관리합니다.
Job이란?
잡(job)은 특정한 단일 작업을 수행하는 작은 작업 단위입니다. 잡은 메서드 호출의 동작과 유사한 방식으로 파라마터를 수신하고 데이터 작업을 수행합니다. 잡은 독립적일 수도 있고, 다른 잡이 먼저 완료된 후에 실행되어야 할 수도 있습니다.
메인 쓰레드에서 사용할 잡을 만들고 이때 사용할 잡들을 대기열에 메인 쓰레드에 임시 혹은 종속적으로 저장합니다. 잡 시스템은 이러한 잡 대기열을 관리하고, 이 대기열에서 작업쓰레드에 잡을 배치 하여 실행합니다. 이러한 대기열을 사용하여 어떤 작업이 언제 실행될지를 관리하며, 실행 순서 및 종속성을 고려합니다. 이러한 과정을 작업 스케줄링(Job Scheduling)이라고 합니다.
작업 스케줄링(Job Scheduling): 개발자는 잡을 정의하고 Unity의 Job System API를 사용하여 작업을 스케줄링합니다. 작업 스케줄링은 잡을 워커 스레드에게 할당하고 실행 순서 및 종속성을 관리합니다. 이렇게 함으로써 병렬 작업을 효과적으로 실행할 수 있습니다.
C# Job System은 이러한 원리로 멀티스레딩 작업을 관리하고 최적화하여 게임 엔진의 성능을 향상합니다.Unity 내부에서 자동으로 워커 스레드를 할당하고 작업을 실행하기 때문에 개발자가 직접 쓰레드 관리에 대한 걱정을 줄일 수 있습니다. 결과적으로 게임의 성능이 향상되고 개발 작업이 단순화됩니다.
NativeContainer
NativeContainer는 Unity의 C# Job System에서 사용되는 메모리 할당 및 관리를 위한 타입 중 하나로, 네이티브 메모리에 상대적으로 안전한 C# 래퍼를 제공합니다. 이것은 병렬 처리 및 안전한 데이터 공유를 위한 중요한 도구 중 하나입니다.
NativeContainer의 주요 특징과 사용 가능한 일부 타입은 다음과 같습니다:
- 안전한 메모리 관리: NativeContainer는 메모리 할당 및 해제를 안전하게 관리합니다. 이것은 개발자가 명시적으로 할당 및 해제에 대해 걱정하지 않아도 되며, 메모리 누수를 방지할 수 있습니다.
- 고성능: NativeContainer는 Unity의 C# Job System과 함께 사용되어 병렬 처리와 멀티스레딩을 효과적으로 지원합니다. 이를 통해 CPU 리소스를 효율적으로 활용하여 성능을 향상할 수 있습니다.
- 안전한 데이터 공유: NativeContainer는 데이터 공유와 동기화를 안전하게 처리합니다. 이것은 여러 스레드 또는 잡이 동일한 데이터에 동시에 접근할 때 데이터 무결성을 보존하는 데 도움이 됩니다.
Allocator 타입: NativeContainer를 생성할 때 할당자(Allocator) 타입을 지정해야 합니다. 할당자는 메모리의 수명과 성능에 영향을 미칩니다. 예를 들어, Allocator.Temp는 가장 빠른 할당이지만 수명이 짧으며, Allocator.Persistent는 오래 지속되지만 상대적으로 느립니다.
Allocator 타입 할당 속도 할당 수명 사용 사례 특징 Allocator.Temp 가장 빠름 1프레임 이하 단기적인 임시 할당에 적합 가장 빠른 할당이지만 수명이 1프레임 이하로 제한됩니다. 프레임이 끝날 때 자동으로 해제되므로 개발자가 명시적으로 Dispose 메서드를 호출할 필요가 없습니다. Allocator.TempJob Temp보다는 느림 4프레임 이하 중간 수명을 가진 임시 할당에 적합 Temp보다는 약간 느리지만, 4프레임 이하의 수명을 가집니다. 스레드 세이프 기능을 지원하므로 잡(Job)에서 사용할 때 유용합니다. 4프레임 내에 Dispose 메서드를 호출하지 않으면 경고가 발생합니다. Allocator.Persistent 가장 느림 애플리케이션 주기 동안 지속 오래 지속되는 메모리 할당에 적합 할당 및 해제가 가장 느리지만, 애플리케이션 주기 동안 지속됩니다. 오래 지속되는 데이터에 사용되며, 명시적으로 해제하지 않는 한 메모리가 해제되지 않습니다. 사용 가능한 NativeContainer 타입: Unity는 여러 종류의 NativeContainer 타입을 제공합니다. 이 중 일부는 다음과 같습니다:
- NativeArray: 크기가 변경되지 않는 NativeContainer입니다.
- NativeList: 크기를 변경할 수 있는 NativeArray의 변형입니다.
- NativeHashMap: 키-값 쌍을 저장하는 NativeContainer입니다.
- NativeMultiHashMap: 하나의 키에 여러 개의 값이 연결된 NativeContainer입니다.
- NativeQueue: 선입선출(FIFO) 대기열을 나타내는 NativeContainer입니다.
DisposeSentinel과 AtomicSafetyHandle: NativeContainer는 DisposeSentinel 및 AtomicSafetyHandle과 함께 제공됩니다. DisposeSentinel은 메모리 누수를 방지하고, AtomicSafetyHandle은 NativeContainer의 소유권을 관리하며 안전한 데이터 공유를 지원합니다.
안전 시스템: NativeContainer는 안전 시스템을 내장하고 있으며, NativeContainer에 대한 읽기 및 쓰기 작업을 추적합니다. 이것은 안전한 데이터 액세스를 보장하며 버그 및 런타임 오류를 방지하는 데 도움이 됩니다.
NativeContainer는 Unity 게임 엔진의 성능 최적화와 멀티스레딩 작업을 관리하는 핵심적인 도구 중 하나이며, 개발자에게 안전하고 효율적인 메모리 관리 및 데이터 공유 방법을 제공합니다.
Job System 사용하기
Unity의 C# Job System을 사용하여 잡(Job)을 만들려면 다음 단계를 따라야 합니다:
1, IJob 인터페이스 구현:
원하는 작업을 수행할 구조체(struct)를 만들고, 이 구조체에 IJob 인터페이스를 구현합니다. IJob 인터페이스는 Unity Job System에서 필수적인 인터페이스로, Execute 메서드를 구현해야 합니다.
인터페이스의 주요 종류에 대해 설명하겠습니다.
인터페이스 설명 IJob IJob 인터페이스는 가장 기본적인 Job 인터페이스로, 단일 스레드에서 동작하는 작업을 정의하는 데 사용됩니다. Execute 메서드를 구현하여 작업 내용을 정의하며, 이 작업은 단일 코어에서 실행됩니다. 주로 독립적인 작업에 사용됩니다. IJobParallelFor IJobParallelFor 인터페이스는 배열 또는 리스트와 같은 데이터 집합을 병렬로 처리하기 위해 사용됩니다. Execute 메서드에 인덱스를 전달하여 각 요소에 대한 작업을 병렬로 수행할 수 있습니다. 이 인터페이스는 데이터의 반복 처리에 적합합니다. IJobParallelForTransform IJobParallelForTransform 인터페이스는 Unity의 Transform을 병렬로 처리하기 위해 사용됩니다. Execute 메서드를 사용하여 여러 개의 Transform을 동시에 업데이트하고 움직일 수 있습니다. 주로 게임 오브젝트의 위치, 회전 등을 업데이트할 때 사용됩니다. IJobParallelForBatch IJobParallelForBatch 인터페이스는 데이터의 일괄(batch) 처리를 위해 사용됩니다. 일괄 처리란 데이터 집합을 여러 작은 그룹으로 나누어 각 그룹을 병렬로 처리하는 것을 의미합니다. 이 인터페이스는 대규모 데이터 집합을 효과적으로 처리할 때 유용합니다. IJobForEach IJobForEach 인터페이스는 Unity의 Entity Component System (ECS)과 함께 사용되며, ECS에서 엔티티 및 컴포넌트를 처리하는 데 사용됩니다. Execute 메서드를 통해 엔티티와 연결된 컴포넌트에 대한 작업을 병렬로 수행할 수 있습니다. IJobChunk IJobChunk 인터페이스는 ECS와 함께 사용되며, 덩어리(chunk) 단위로 엔티티를 처리하는 데 사용됩니다. 덩어리는 컴포넌트의 그룹을 나타내며, Execute 메서드를 통해 덩어리에 대한 작업을 병렬로 수행합니다. 대량의 엔티티 처리에 적합합니다. 각 Job 인터페이스는 특정한 작업 유형과 작업 환경에 맞게 설계되었습니다. Unity의 C# Job System을 사용하면 멀티스레딩 작업을 효과적으로 관리하고 성능을 향상시킬 수 있으므로, 적절한 Job 인터페이스를 선택하여 개발 작업을 수행하는 것이 중요합니다.
아래는 간단한 예제입니다:
using Unity.Jobs; public struct MyJob : IJob { public void Execute() { // 잡의 실제 작업 내용을 이곳에 구현 } }
2, 멤버 변수 추가:
잡이 사용할 데이터를 나타내는 멤버 변수를 구조체에 추가합니다. 이 데이터는 일반적으로 Blittable 타입 또는 NativeContainer 타입이어야 합니다. Blittable 타입은 일반적인 데이터 타입으로서 원시 데이터 유형(int, float, 등)과 유사한 성질을 갖습니다. NativeContainer 타입은 Unity의 네이티브 컨테이너를 사용하여 스레드 간 데이터 공유를 가능하게 합니다.
public struct MyJob : IJob { public NativeArray<float> inputData; // NativeContainer 예시 public NativeArray<float> outputData; // NativeContainer 예시 public void Execute() { // inputData를 사용하여 outputData에 작업을 수행 } }
3, Execute 메서드 구현:
Execute 메서드 내에서 실제 작업을 구현합니다. 이 메서드는 단일 코어에서 실행됩니다.
잡이 필요한 작업을 수행하고 결과를 출력 데이터에 저장합니다.
4, 잡 스케줄링:
작성한 잡을 스케줄링하여 실행합니다. 이는 주로 메인 스레드에서 이루어집니다.
아래는 잡을 스케줄링하고 완료하기 위한 예시 코드입니다:
MyJob myJob = new MyJob(); myJob.inputData = inputData; // 데이터 설정 myJob.outputData = outputData; // 데이터 설정 JobHandle jobHandle = myJob.Schedule(); // 작업 스케줄링 // 다른 작업 스케줄링 또는 작업 종속성 설정 가능 jobHandle.Complete(); // 작업 완료 대기
예시 코드
아래는 JobSystem에 대한 간단한 예시 코드입니다.
using UnityEngine; using UnityEngine.Jobs; using Unity.Jobs; using Unity.Entities; public class SpawnParallel : MonoBehaviour { public GameObject sheepPrefab; // 양 프리팹 Transform[] AllSheepTransform; // 모든 양의 트랜스폼 배열 public int numSheep = 10000; // 양의 개수 설정 struct MoveJob : IJobParallelForTransform { public void Execute(int index, TransformAccess transform) { // 이동 작업을 수행하는 잡(Job) 구조체 // 양을 위으로 이동시킵니다. transform.position += 0.1f * (transform.rotation * new Vector3(0, 1, 0)); // 양이 일정 높이(여기서는 50)를 넘어가면 땅으로 되돌립니다. if (transform.position.y > 50) { transform.position = new Vector3(transform.position.x, 0, transform.position.z); } } } MoveJob moveJob; // 이동 작업을 위한 잡(Job) JobHandle moveHandle; // 작업 핸들 TransformAccessArray transforms; // 양들의 트랜스폼에 대한 배열 // Start is called before the first frame update void Start() { // 양들의 트랜스폼 배열 초기화 AllSheepTransform = new Transform[numSheep]; // 지정된 개수만큼 양을 무작위 위치에 생성하고 트랜스폼을 배열에 저장 for (int i = 0; i < numSheep; i++) { Vector3 pos = new Vector3(Random.Range(-50, 50), Random.Range(-50, 50), Random.Range(-50, 50)); GameObject obj = Instantiate(sheepPrefab, pos, Quaternion.identity); AllSheepTransform[i] = obj.transform; } // 양들의 트랜스폼에 대한 접근 배열 생성 transforms = new TransformAccessArray(AllSheepTransform); } private void Update() { // 이동 작업을 수행할 잡(Job) 생성 moveJob = new MoveJob { }; // 잡(Job)을 스케줄링하여 양들의 이동 수행 moveHandle = moveJob.Schedule(transforms); } private void LateUpdate() { // 이동 작업이 완료될 때까지 대기 moveHandle.Complete(); } private void OnDestroy() { // 사용한 자원 정리 transforms.Dispose(); } }
실행결과
마무리
Unity Job System은 강력하지만 복잡한 도구입니다. 그러므로 깊게 학습하고 신중하게 활용함으로써 그 장점을 최대화 할 수 있습니다. 앞으로도 계속해서 연구와 실험을 진행하여 게임 개발에 최적화된 멀티스레딩 전략을 구축하길 바랍니다. 이를 통해 더욱 효율적이고 성능이 우수한 게임을 개발할 수 있을 것입니다.
반응형다음글이전글이전 글이 없습니다.댓글