Unity/DOTS

Unity C# Job System - 메뉴얼 정리

JAVART 2020. 7. 29. 10:49
반응형

(정보 : 단순히 기제된 내용을 스크랩 해온 것 입니다.)

(주의 : 영어는 부족한 실력으로 간단히 번역주석을 달았습니다. 참고만 하시고 직접 영어 보시면서 해석해주세요.)

https://docs.unity3d.com/kr/current/Manual/JobSystem.html

C# 잡 시스템 개요

C# 잡 시스템의 동작 방식

Unity C# 잡 시스템을 통해 사용자는 나머지 Unity 기능과 잘 연동하고 수정 코드 작성을 용이하게 해주는 멀티스레드 코드를 작성할 수 있습니다.

멀티스레드 코드를 작성하면 성능이 향상되는 이점을 누릴 수 있으며, 프레임 속도도 대폭 개선됩니다. 버스트 컴파일러를 C# 잡과 함께 사용하면 코드 생성 품질이 개선되며, 모바일 디바이스의 배터리 소모량도 크게 감소합니다.

C# 잡 시스템의 핵심은 Unity의 내부 기능(Unity의 네이티브 잡 시스템)과 통합된다는 점입니다. 사용자가 작성한 코드와 Unity는 동일한 워커 스레드를 공유합니다. 이러한 협력을 이용하면 CPU 코어보다 많은 스레드를 만들지 않아도 되므로 CPU 리소스에 대한 경쟁을 피할 수 있습니다.

자세한 내용은 Unity GDC - Job 시스템과 엔티티 컴포넌트 시스템 강연을 참조하십시오.

 

멀티스레딩이란?

단일 스레드 컴퓨팅 시스템에서는 한 번에 하나의 명령어가 입력되고 한 번에 하나의 결과가 출력됩니다. 프로그램을 로드하고 완료하는 데 걸리는 시간은 CPU가 수행해야 하는 작업량에 따라 다릅니다.

멀티스레딩은 여러 코어에서 한 번에 여러 개의 스레드를 처리하는 CPU 성능을 활용하는 프로그래밍의 한 유형입니다. 한 번에 하나가 아니라, 동시에 여러 개의 작업 또는 명령을 실행합니다.

어떤 스레드는 기본적으로 프로그램이 시작할 때 실행됩니다. 이 스레드가 바로 ’메인 스레드’입니다. 메인 스레드는 작업을 처리하기 위해 새로운 스레드를 생성합니다. 이러한 새 스레드는 다른 스레드와 병렬로 실행되며, 대개 실행이 완료되면 메인 스레드와 결과를 동기화합니다.

이러한 멀티스레딩 방식은 여러 개의 작업이 오랫동안 실행되는 경우에 적합합니다. 하지만 일반적으로 게임 개발 코드에는 한 번에 실행해야 할 작은 명령이 많이 들어 있습니다. 각 명령에 대해 스레드를 만들면 그 수가 너무 많아지고 각각의 수명도 짧아집니다. 따라서 CPU 및 운영체제의 프로세싱 능력을 초과할 수 있습니다.

스레드 풀을 사용하면 스레드 수명 주기 문제를 완화할 수 있습니다. 하지만 스레드 풀을 사용해도 동시에 활성화된 스레드 수가 너무 많을 수 있습니다. CPU 코어보다 스레드 수가 더 많으면 CPU 리소스를 놓고 스레드 간에 경쟁이 벌어지고, 이로 인해 컨텍스트 스위칭이 빈번하게 발생합니다. 컨텍스트 스위칭은 실행 도중에 스레드 상태를 저장하고 다른 스레드에 대한 작업을 진행한 후 첫 번째 스레드를 재구성하여 나중에 계속 처리하는 프로세스입니다. 컨텍스트 스위칭은 리소스를 매우 많이 소모하므로 가급적 사용하지 않는 것이 좋습니다.

 

잡 시스템이란?

잡 시스템(job system)은 스레드를 대신하여 을 만들어 멀티스레드 코드를 관리합니다.

잡 시스템은 여러 코어에 걸쳐 워커 스레드 그룹을 관리합니다. 컨텍스트가 바뀌지 않도록 하기 위해 일반적으로 CPU 논리 코어당 하나의 워커 스레드가 있지만, 시스템이 운영체제나 기타 전용 애플리케이션에서 사용할 코어 몇 개를 예약해 둘 수 있습니다.

잡 시스템은 잡 대기열에 잡을 배치하여 실행합니다. 잡 시스템의 워커 스레드는 잡 대기열에서 항목을 가져와 실행합니다. 잡 시스템은 종속성을 관리하고 작업이 올바른 순서대로 실행되도록 합니다.

잡이란?

잡(job)은 특정한 단일 작업을수행하는 작은 작업 단위입니다. 잡은 메서드 호출의 동작과 유사한 방식으로 파라마터를 수신하고 데이터 작업을 수행합니다. 잡은 독립적일 수도 있고, 다른 잡이 먼저 완료된 후에 실행되어야 할 수도 있습니다.

잡 종속성이란?

게임 개발에 필요한 시스템처럼 복잡한 시스템에서는 각 잡이 독립적일 가능성이 낮습니다. 이 경우 잡은 일반적으로 다음 잡에 사용할 데이터를 준비하는데, 이를 위해 종속성을 인식하고 지원합니다. jobA가 jobB에 종속된 경우 잡 시스템은 jobB가 완료될 때까지 jobA가 실행되지 않도록 합니다.

 

C# 잡 시스템의 안전 시스템

경쟁 상태

멀티스레드 코드를 작성할 때는 항상 경쟁 상태가 발생할 위험이 있습니다. 경쟁 상태는 통제할 수 없는 다른 프로세스의 타이밍에 따라 작업의 결과가 달라지는 경우에 발생합니다.

경쟁 상태가 반드시 버그는 아니지만, 비결정론적인 동작의 원인이 됩니다. 경쟁 상태로 인해 버그가 발생한 경우, 타이밍에 따라 원인이 달라지기 때문에 특수한 경우를 제외하고는 문제를 재현할 수 없어 근본적인 원인을 식별하기가 어려울 수 있습니다. 이러한 문제를 디버깅하면 중단점과 로깅에 따라 개별 스레드의 타이밍이 바뀌므로 문제가 사라질 수 있습니다. 경쟁 상태는 멀티스레드 코드 작성 시 가장 중대한 문제를 유발할 수 있습니다.

안전 시스템

멀티스레드 코드를 더 쉽게 작성할 수 있도록 Unity C# 잡 시스템은 모든 잠재적인 경쟁 상태를 감지하고 그로 인해 발생할 수 있는 버그를 차단합니다.

예를 들어, C# 잡 시스템에서 메인 스레드에 있는 코드의 데이터에 대한 레퍼런스를 잡으로 전송하는 경우, 잡이 데이터를 쓰는 동시에 메인 스레드가 데이터를 읽는지 시스템이 확인할 수 없습니다. 이 경우 경쟁 상태가 발생합니다.

C# 잡 시스템은 각 잡에 메인 스레드의 데이터에 대한 레퍼런스를 보내지 않고 작업이 필요한 데이터를 보내 이 문제를 해결합니다. 이 복사본은 데이터를 격리시키므로 경쟁 상태가 발생하지 않습니다.

C# 잡 시스템이 데이터를 복사하는 방법으로 인해 잡은 blittable 데이터 타입에만 액세스할 수 있습니다. 이 데이터 타입을 관리되는 코드와 네이티브 코드 간에 전달하는 경우 변환할 필요가 없습니다.

C# 잡 시스템은 memcpy를 사용하여 blittable 타입을 복사하고 Unity의 관리되는 파트와 네이티브 파트 간에 데이터를 전송할 수 있습니다. 시스템은 잡을 예약할 때 memcpy를 사용하여 데이터를 네이티브 메모리에 저장하고, 잡을 실행할 때 이 복사본에 액세스할 수 있는 권한을 관리되는 파트에 할당합니다. 자세한 내용은 잡 예약을 참조하십시오.

 

NativeContainer

데이터 복사의 안전 시스템 프로세스에 대한 단점은 잡의 결과가 각 복사본 내에 격리된다는 것입니다. 이러한 제한을 극복하려면 결과를 NativeContainer라고 불리는 공유 메모리 타입에 저장해야 합니다.

NativeContainer란?

NativeContainer는 네이티브 메모리에 상대적으로 안전한 C# 래퍼를 제공하는 관리되는 값 타입입니다. 여기에는 관리되지 않는 할당에 대한 포인터가 들어 있습니다. Unity C# 잡 시스템과 함께 NativeContainer를 사용하면 잡이 복사본으로 작업하는 것이 아니라 메인 스레드와 공유되는 데이터에 액세스할 수 있습니다.

이용할 수 있는 NativeContainer 타입은?

Unity는 NativeArray라고 불리는 NativeContainer와 함께 제공됩니다. 또한 NativeSlice NativeArray를 조작하여 특정 포지션에서 NativeArray의 하위 집합을 특정 길이로 가져올 수도 있습니다.

참고: 엔티티 컴포넌트 시스템(ECS) 패키지는 다른 타입의 NativeContainer를 포함하도록 Unity.Collections 네임스페이스를 확장합니다.

  • NativeList - 크기 변경이 가능한 NativeArray입니다.
  • NativeHashMap - 키 및 값 쌍입니다.
  • NativeMultiHashMap - 키당 여러 개의 값입니다.
  • NativeQueue - 선입선출(FIFO) 대기열입니다.

NativeContainer 및 안전 시스템

안전 시스템은 모든 NativeContainer 타입에 내장되어 있으며, NativeContainer에 대한 읽기/쓰기 작업을 추적합니다.

참고: NativeContainer 타입에 대한 모든 안전 검사(예: 한도 검사, 할당 취소 검사, 경쟁 상태 검사)는 Unity Editor  Play Mode 에서 이용할 수 있습니다.

안전 시스템에는 DisposeSentinel AtomicSafetyHandle도 포함되어 있습니다. DisposeSentinel은 메모리 누수를 검사한 후 메모리를 잘못 할당한 경우 오류를 표시합니다. 메모리 누수 오류에 대한 트리거는 누수 후 오랜 시간이 지난 다음에 일어납니다.

AtomicSafetyHandle을 사용하면 코드로 NativeContainer의 소유권을 이전할 수 있습니다. 예를 들어, 두 개의 예약된 잡이 동일한 NativeArray에 작성하면 안전 시스템에서 예외가 발생하고 문제에 대한 이유 및 해결 방법을 설명하는 오류 메시지를 표시합니다. 문제가 되는 잡을 예약하면 안전 시스템에서 예외가 발생합니다.

이 경우 종속성을 사용하여 잡을 예약할 수 있습니다. 첫 번째 잡은 NativeContainer에 작성하고, 실행이 완료되면 다음 잡이 동일한 NativeContainer에 안전하게 작성하고 읽을 수 있습니다. 이러한 읽기/쓰기 제한은 메인 스레드에서 데이터에 액세스할 때도 적용됩니다. 안전 시스템을 사용하면 여러 작업이 동일한 데이터에서 병렬로 읽을 수 있습니다.

기본적으로 잡이 NativeContainer에 대한 액세스 권한을 가진 경우 읽기와 쓰기 모두를 수행할 수 있습니다. 이러한 설정은 성능 저하를 야기할 수 있습니다. C# 잡 시스템은 NativeContainer에 작성 중인 잡이 있을 때는 다른 잡에 쓰기 권한을 허용하지 않습니다.

잡이 NativeContainer에 작성할 필요가 없다면 [ReadOnly] 속성을 사용하여 다음과 같이 NativeContainer를 표시하십시오.

[ReadOnly] public NativeArray<int> input;

위 예에서는 첫 번째 NativeArray에 대한 읽기 전용 액세스 권한이 있는 다른 잡과 동시에 잡을 실행할 수 있습니다.

참고: 잡 내에서 정적 데이터에 대한 액세스는 보호되지 않습니다. 정적 데이터에 액세스하면 모든 안전 시스템을 우회하므로 Unity에 크래시가 발생할 수 있습니다. 자세한 내용은 C# 잡 시스템 팁 및 문제 해결을 참조하십시오.

NativeContainer 할당자

NativeContainer를 만들 때는 필요한 메모리 할당 타입을 지정해야 합니다. 할당 타입은 잡 실행 시간에 따라 다릅니다. 이렇게 하면 할당을 맞춤 설정하여 각 상황에서 최고의 성능을 끌어낼 수 있습니다.

NativeContainer 메모리 할당 및 릴리스에는 세 가지의 할당자 타입을 이용할 수 있습니다. NativeContainer를 인스턴스화할 때 적절한 타입을 지정해야 합니다.

  • Allocator.Temp는 가장 빠른 할당입니다. 수명이 1프레임 이하인 할당에 적합합니다. Temp를 사용하여 잡에 NativeContainer 할당을 전달하면 안 됩니다. 또한 메서드 호출(예: MonoBehaviour.Update 또는 네이티브에서 관리되는 코드로의 기타 다른 콜백)을 반환하기 전에 Dispose 메서드를 호출해야 합니다.
  • Allocator.TempJob Temp보다는 느리지만 Persistent보다는 빠른 할당입니다. 수명이 4프레임 이하인 할당에 적합하며 스레드 세이프 기능을 지원합니다. 4프레임 내에서 Dispose 메서드를 호출하지 않으면 콘솔은 네이티브 코드로 생성된 경고를 출력합니다. 대부분의 소규모 잡은 이 NativeContainer 할당 타입을 사용합니다.
  • Allocator.Persistent는 가장 느린 할당이지만, 애플리케이션의 주기에 걸쳐 필요한 만큼 오래 지속됩니다. malloc에 대한 직접 호출을 위한 래퍼입니다. 오래 걸리는 잡은 이 NativeContainer 할당 타입을 사용할 수 있습니다. 성능이 중요한 상황에서는 Persistent를 사용하지 않아야 합니다.

예제:

NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

참고: 위 예제에서 숫자 1은 NativeArray의 크기를 나타냅니다. 이 경우에는 하나의 어레이 요소만 보유하고 있습니다(하나의 데이터 조각만 result에 저장함).

 

잡 만들기

Unity에서 잡을 만들려면 IJob 인터페이스를 구현해야 합니다. IJob을 사용하면 실행 중인 다른 잡과 병렬로 실행되는 단일 잡을 예약할 수 있습니다.

참고: ’잡’은 IJob 인터페이스를 구현하는 구조체에 관한 Unity의 포괄적인 용어입니다.

잡을 만들려면 다음을 수행해야 합니다.

  • IJob을 구현하는 구조체를 만듭니다.
  • 해당 잡이 사용하는 멤버 변수(blittable 타입 또는 NativeContainer 타입)를 추가합니다.
  • 구조체에 Execute 메서드를 만들고 그 안에서 잡을 구현합니다.

잡을 실행할 때 Execute 메서드는 단일 코어에서 실행됩니다.

참고: 잡을 디자인할 때는 데이터 복사본에서 동작한다는 점을 기억하십시오(NativeContainer의 경우는 예외). 따라서 메인 스레드에서 잡의 데이터에 액세스하는 유일한 방법은 NativeContainer에 작성하는 것입니다.

간단한 잡 정의 예시

// Job adding two floating point values together
// Job은 두개의 플로팅 소수점 값을 함께 더합니다.
public struct MyJob : IJob
{
    public float a;
    public float b;
    public NativeArray<float> result;

    public void Execute()
    {
        result[0] = a + b;
    }
}

 

잡 예약

메인 스레드에서 잡을 예약하려면 다음을 수행해야 합니다.

  • 잡을 인스턴스화합니다.
  • 잡의 데이터를 채웁니다.
  • Schedule 메서드를 호출합니다.

Schedule을 호출하면 적절한 시점에 실행되도록 잡을 잡 대기열에 넣습니다. 예약된 잡은 인터럽트할 수 없습니다.

참고: 메인 스레드에서는 Schedule만 호출할 수 있습니다.

잡 예약 예시

// Create a native array of a single float to store the result. This example waits for the job to complete for illustration purposes
// 결과를 저장하기 위한 단일 float NativeArray를 만듭니다. 이 예는 설명을 위해 작업이 완료되기를 기다립니다.
NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

// Set up the job data
MyJob jobData = new MyJob();
jobData.a = 10;
jobData.b = 10;
jobData.result = result;

// Schedule the job
// 작업 예약
JobHandle handle = jobData.Schedule();

// Wait for the job to complete
// 작업이 끝나길 기다립니다.
handle.Complete();

// All copies of the NativeArray point to the same memory, you can access the result in "your" copy of the NativeArray
// NativeArray의 모든 사본이 동일한 메모리를 가리킨다면, "본인"의 결과값에 접근이 가능합니다.
float aPlusB = result[0];

// Free the memory allocated by the result array
// 결과 배열에 의해 할당된 메모리를 정리합니다.
result.Dispose();

 

JobHandle 및 종속성

잡의 Schedule 메서드를 호출하면 JobHandle을 반환합니다. 코드의 JobHandle을 다른 잡에 대한 종속성으로 사용할 수 있습니다. 잡이 다른 잡의 결과에 종속되면 첫 번째 잡의 JobHandle을 파라미터로 두 번째 잡의 Schedule 메서드에 다음과 같이 전달할 수 있습니다.

JobHandle firstJobHandle = firstJob.Schedule();
secondJob.Schedule(firstJobHandle);

종속성 결합

잡에 종속성이 많은 경우에는 JobHandle.CombineDependencies 메서드를 사용하여 결합할 수 있습니다. CombineDependencies를 이용하면 종속성을 Schedule 메서드로 전달할 수 있습니다.

NativeArray<JobHandle> handles = new NativeArray<JobHandle>(numJobs, Allocator.TempJob);

// Populate `handles` with `JobHandles` from multiple scheduled jobs...
// 여러 예약된 작업에서 'JobHandles'를 이용해 'handles'를 채웁니다.

JobHandle jh = JobHandle.CombineDependencies(handles);

메인 스레드에서 잡 대기

JobHandle을 사용하면 코드가 메인 스레드에서 잡의 실행이 끝날 때까지 기다리도록 만들 수 있습니다. 이렇게 하려면 JobHandle에 대해 Complete 메서드를 호출하십시오. 그러면 메인 스레드가 잡이 사용하던 NativeContainer에 안전하게 액세스할 수 있습니다.

참고: 잡을 예약할 때는 잡이 실행되지 않습니다. 메인 스레드에서 잡을 대기하고 있고 잡이 사용하는 NativeContainer 데이터에 액세스해야 하는 경우 JobHandle.Complete 메서드를 호출할 수 있습니다. 이 메서드는 메모리 캐시의 잡을 플러시하고 실행 프로세스를 시작합니다. JobHandle에 대해 Complete를 호출하면 해당 잡의 NativeContainer 타입에 대한 소유권을 메인 스레드에 반환합니다. 메인 스레드에서 해당 NativeContainer 타입에 다시 안전하게 액세스하려면 JobHandle에 대해 Complete를 호출해야 합니다. 잡 종속성에서 온 JobHandle에 대해 Complete를 호출하여 소유권을 메인 스레드로 반환할 수도 있습니다. 예를 들어, jobA에 대해 Complete를 호출하거나, jobA에 종속된 jobB에 대해 Complete를 호출할 수 있습니다. 두 경우 모두 Complete 호출 이후 메인 스레드에서 jobA가 사용하는 NativeContainer 타입에 안전하게 액세스할 수 있습니다.

데이터에 액세스할 필요가 없는 경우에는 배치를 명시적으로 플러시해야 합니다. 이렇게 하려면 JobHandle.ScheduleBatchedJobs 정적 메서드를 호출하십시오. 이 메서드를 호출하면 성능이 저하될 수 있습니다.

여러 개의 잡과 종속성 예시

잡 코드:

// Job adding two floating point values together
public struct MyJob : IJob
{
    public float a;
    public float b;
    public NativeArray<float> result;

    public void Execute()
    {
        result[0] = a + b;
    }
}

// Job adding one to a value
// 값에 1을 더하는 작업
public struct AddOneJob : IJob
{
    public NativeArray<float> result;
    
    public void Execute()
    {
        result[0] = result[0] + 1;
    }
}

메인 스레드 코드:

// Create a native array of a single float to store the result in. This example waits for the job to complete NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob); // Setup the data for job #1 MyJob jobData = new MyJob(); jobData.a = 10; jobData.b = 10; jobData.result = result; // Schedule job #1 JobHandle firstHandle = jobData.Schedule(); // Setup the data for job #2 AddOneJob incJobData = new AddOneJob(); incJobData.result = result; // Schedule job #2 JobHandle secondHandle = incJobData.Schedule(firstHandle); // Wait for job #2 to complete secondHandle.Complete(); // All copies of the NativeArray point to the same memory, you can access the result in "your" copy of the NativeArray float aPlusB = result[0]; // Free the memory allocated by the result array result.Dispose();

// Create a native array of a single float to store the result in. This example waits for the job to complete
NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

// Setup the data for job #1
// 작업 #1에 대한 데이터를 셋업합니다.
MyJob jobData = new MyJob();
jobData.a = 10;
jobData.b = 10;
jobData.result = result;

// Schedule job #1
// 작업 #1을 예약합니다.
JobHandle firstHandle = jobData.Schedule();

// Setup the data for job #2
// 작업 #2에 대한 데이터를 셋업합니다.
AddOneJob incJobData = new AddOneJob();
incJobData.result = result;

// Schedule job #2
// 작업 #2를 예약합니다.
JobHandle secondHandle = incJobData.Schedule(firstHandle);

// Wait for job #2 to complete
// 작업 #2가 완료되기를 기다립니다.
secondHandle.Complete();

// All copies of the NativeArray point to the same memory, you can access the result in "your" copy of the NativeArray
float aPlusB = result[0];

// Free the memory allocated by the result array
result.Dispose();

 

ParallelFor 잡

잡을 예약할 경우 하나의 잡은 하나의 작업만 수행할 수 있습니다. 게임에서는 수많은 오브젝트에 대해 동일한 작업을 수행하는 경우가 흔합니다. 따라서 이를 위해 IJobParallelFor라고 불리는 별도의 잡 타입이 제공됩니다.

참고: ’ParallelFor’는 IJobParallelFor 인터페이스를 구현하는 구조체에 관한 Unity의 포괄적인 용어입니다.

ParallelFor 잡은 데이터의 NativeArray를 사용하여 데이터 소스로 동작합니다. ParallelFor 잡은 여러 개의 코어에서 동작합니다. 코어당 하나의 잡이 있으며, 각각 일정량의 작업을 처리합니다. IJobParallelFor는 IJob과 비슷하게 동작하지만, 단일 Execute가 아니라 데이터 소스의 항목당 하나의 Execute 메서드를 호출합니다. Execute 메서드에는 정수 파라미터가 있습니다. 이 인덱스는 잡 구현 내에서 데이터 소스의 단일 요소에 액세스하여 동작합니다.

ParallelFor 잡 정의 예시

struct IncrementByDeltaTimeJob: IJobParallelFor
{
    public NativeArray<float> values;
    public float deltaTime;

    public void Execute (int index)
    {
        float temp = values[index];
        temp += deltaTime;
        values[index] = temp;
    }
}

ParallelFor 잡 예약

ParallelFor 잡을 예약할 때는 분할할 NativeArray 데이터 소스의 길이를 지정해야 합니다. Unity C# 잡 시스템은 구조체에 여러 개의 NativeArray가 있으면 사용자가 어느 것을 데이터 소스로 사용할지 알 수 없습니다. 또한 데이터 소스의 길이는 C# 잡 시스템에 예상되는 Execute 메서드 개수도 알려줍니다.

보이지 않는 곳에서 ParallelFor 잡 예약은 더 복잡하게 동작합니다. ParallelFor 잡을 예약할 때 C# 작업 시스템은 작업을 배치로 나누어 코어 간에 배포합니다. 각 배치에는 Execute 메서드의 하위 집합이 포함됩니다. 그러면 C# 잡 시스템은 Unity의 네이티브 잡 시스템에서 CPU 코어당 하나의 잡을 예약한 후 해당 네이티브 잡에 완료할 일부 배치를 전달합니다.

여러 코어 간에 배치를 나누는 ParallelFor 잡

한 네이티브 잡이 다른 네이티브 잡보다 배치를 먼저 완료하면 다른 네이티브 잡의 남은 배치를 가져옵니다. 한 번에 네이티브 잡의 남은 배치의 절반만 가져오므로 캐시 집약성이 보장됩니다.

프로세스를 최적화하려면 배치 수를 지정해야 합니다. 배치 수는 몇 개의 잡을 가져오고 스레드 간에 작업 재배포를 어떻게 세부 조정할지를 제어합니다. 작은 배치 수(예:1)를 지정하면 스레드 간에 작업을 더 균등하게 배포할 수 있습니다. 성능 소모가 더 발생하더라도 때로는 배치 수를 늘려야 할 때도 있습니다. 1부터 시작하여 성능 향상을 무시할 수 있는 수준까지 개수를 늘리는 것도 좋은 전략입니다.

ParallelFor 잡 예약 예시

잡 코드:

 

// Job adding two floating point values together
public struct MyParallelJob : IJobParallelFor
{
    [ReadOnly]
    public NativeArray<float> a;
    [ReadOnly]
    public NativeArray<float> b;
    public NativeArray<float> result;

    public void Execute(int i)
    {
        result[i] = a[i] + b[i];
    }
}

메인 스레드 코드:

 

NativeArray<float> a = new NativeArray<float>(2, Allocator.TempJob);

NativeArray<float> b = new NativeArray<float>(2, Allocator.TempJob);

NativeArray<float> result = new NativeArray<float>(2, Allocator.TempJob);

a[0] = 1.1;
b[0] = 2.2;
a[1] = 3.3;
b[1] = 4.4;

MyParallelJob jobData = new MyParallelJob();
jobData.a = a;  
jobData.b = b;
jobData.result = result;

// Schedule the job with one Execute per index in the results array and only 1 item per processing batch
// 결과 배열에서 인덱스 당 하나의 실행과 처리 배치 당 하나의 항목만으로 작업을 예약합니다.
JobHandle handle = jobData.Schedule(result.Length, 1);

// Wait for the job to complete
// 작업이 완료되기를 기다립니다.
handle.Complete();

// Free the memory allocated by the arrays
// 메모리를 정리합니다.
a.Dispose();
b.Dispose();
result.Dispose();

 

ParallelForTransform 잡

ParallelForTransform 잡은 또 다른 타입의 ParallelFor 잡으로, 트랜스폼에서 동작하도록 특별히 디자인되었습니다.

참고: ParallelForTransform 잡은 IJobParallelForTransform 인터페이스를 구현하는 잡에 관한 Unity의 포괄적인 용어입니다.

 

C# 잡 시스템 팁 및 문제 해결

Unity C# 잡 시스템을 사용할 때는 다음 사항을 준수해야 합니다.

잡에서 정적 데이터에 액세스하지 않기

잡에서 정적 데이터에 액세스하면 모든 안전 시스템을 우회하게 됩니다. 잘못된 데이터에 액세스하면 Unity에 예상치 못한 크래시가 발생할 수 있습니다. 예를 들어, MonoBehaviour에 액세스하면 도메인을 다시 로드할 때 크래시가 일어납니다.

참고: 이러한 위험성 때문에 향후 Unity 버전에서는 정적 분석을 사용하여 잡이 전역 변수에 액세스하지 못하도록 막을 예정입니다. 향후 Unity 버전의 경우 잡 내에서 정적 데이터에 액세스하면 코드가 손상될 수 있습니다.

예약된 배치를 플러시하기

잡 실행을 시작하려는 경우 JobHandle.ScheduleBatchedJobs를 사용하여 예약된 배치를 플러시할 수 있습니다. 이 메서드를 호출하면 성능 저하가 발생할 수 있습니다. 배치를 플러시하지 않으면 메인 스레드가 결과를 기다릴 때까지 예약이 지연됩니다. 그외 다른 경우에는 JobHandle.Complete을 사용하여 실행 프로세스를 시작하십시오.

참고:엔티티 컴포넌트 시스템(ECS)에서 배치는 암시적으로 플러시되므로 JobHandle.ScheduleBatchedJobs를 호출할 필요가 없습니다.

NativeContainer 콘텐츠를 업데이트하지 않기

레퍼런스 반환의 부재로 인해 NativeContainer의 콘텐츠를 직접 변경할 수 없습니다. 예를 들어, nativeArray[0]++;는 nativeArray에서 값을 업데이트하지 않는 var temp = nativeArray[0]; temp++;를 작성하는 것과 동일합니다.

대신에 인덱스의 데이터를 로컬 임시 복사본으로 복사하고 해당 복사본을 수정한 후 다음과 같이 다시 저장해야 합니다.

MyStruct temp = myNativeArray[i];
temp.memberVariable = 0;
myNativeArray[i] = temp;

JobHandle.Complete를 호출하여 소유권 다시 얻기

데이터 소유권을 추적하려면 종속성을 완료하여 메인 스레드가 다시 사용할 수 있도록 해야 합니다. JobHandle.IsCompleted을 확인하는 것만으로는 부족합니다. JobHandle.Complete 메서드를 호출하여 NativeContainer 타입의 소유권을 다시 얻어 메인 스레드에 제공해야 합니다. 또한 Complete를 호출하면 안전 시스템의 상태도 정리됩니다. 이렇게 하지 않으면 메모리 누수가 발생합니다. 이전 프레임의 잡에 대한 종속성이 있는 모든 프레임마다 새 잡을 예약하는 경우에도 이 프로세스가 적용됩니다.

메인 스레드에서 예약 및 완료 사용

메인 스레드에서는 Schedule  Complete만 호출할 수 있습니다. 한 잡이 다른 잡에 종속되는 경우에는 잡 내에서 잡을 예약하지 말고 JobHandle을 사용하여 종속성을 관리하십시오.

적시에 예약 및 완료 사용

잡이 필요한 데이터를 얻은 후 잡에 대해 Schedule을 호출하고, 결과를 얻을 때까지 잡에 대해 Complete를 호출하지 마십시오. 잡을 예약할 때는 실행 중인 다른 잡과 경쟁하지 않을 때까지 기다릴 필요가 없도록 해야 합니다. 예를 들어, 한 프레임의 끝과 다른 프레임의 시작 간에 시간 간격이 있고 아무런 잡도 실행되지 않으며 한 프레임의 지연이 용인할 수 있는 수준인 경우에는 잡을 프레임 끝에 가깝게 예약하고 그 결과를 다음 프레임에 사용할 수 있습니다. 또는 게임에서 다른 잡과의 전환 기간이 포화 상태이고 프레임 내 어딘가에 충분히 활용하지 않은 시간이 있는 경우에는 해당 지점에서 잡을 예약하는 것이 더 효율적입니다.

NativeContainer 타입을 읽기 전용으로 표시

잡에는 기본적으로 NativeContainer 타입에 대한 읽기/쓰기 권한이 있습니다. 적절한 경우 [ReadOnly] 속성을 사용하여 성능을 향상시키십시오.

데이터 종속성 검사

Unity Profiler 창에서 메인 스레드의 “WaitForJobGroup” 마커는 Unity가 워커 스레드에 대한 잡이 완료되기를 기다리고 있음을 나타냅니다. 또한 어딘가에서 데이터 종속성이 사용되고 있어 해결이 필요함을 의미할 수도 있습니다. JobHandle.Complete를 찾아보면 메인 스레드를 대기하도록 만드는 데이터 종속성의 위치를 추적할 수 있습니다.

잡 디버깅

잡에는 Schedule 대신에 사용하여 메인 스레드의 잡을 즉시 실행할 수 있는 Run 함수가 있습니다. 이 함수는 디버깅 목적으로도 사용할 수 있습니다.

잡에서 관리되는 메모리 할당하지 않기

잡에서 관리되는 메모리를 할당하면 속도가 매우 느려지고, 잡이 Unity Burst 컴파일러를 충분히 활용하여 성능을 향상할 수 없습니다. Burst는 여러 이점을 제공하는 새로운 LLVM 기반 백엔드 컴파일러 기술입니다. C# 잡을 사용하여 플랫폼의 특정 기능을 활용하여 고도로 최적화된 기계어 코드를 생성합니다.

추가 정보

반응형