페이지

2021년 2월 25일 목요일

Unity C# Job System 3

 Creating Jobs

  • Job을 만들기 위해서는 IJob 인터페이스를 구현한다
  • 해당 Job 사용할 멤버변수들을 추가한다
  • Execute 함수를 Job내에 구현한다.

Jobs이 수행되면, Execute 함수는 하나의 코어에 한번 실행된다.

Note :  Job을 설계할때, NativeContainer가 아닌것 data들은 복사본이라는 것을 기억해야 한다. 그래서 메인스레드에서 data를 접근하는 유일한 방법은 NativeContainer를 사용하는 것 밖에 없다.

public struct MyJob:IJob {
    public float a;
    public float b;
    public NativeArray<float> result;
    public void Execute(){
        result[0] = a + b;
    }
}


Scheduling Jobs

메인스레드에서 Job을 스케줄하기 위해서는
  • Instantiate the job
  • Populate the Job's data
  • Call the Schedule method
Schedule함수를 호출하는 것은 해당 Job을 Job Queued에 넣는 다는 것을 의미한다. 그러면 시스템은 적절한 시간에 그 Job을 수행한다. 한번 Schedule 되었으면, 너는 그것을 방해할수 없다.
단지, 메인스레드에서 Schedule 함수만 호출할수 있다.


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

MyJob jobData = new MyJob();
jobData.a = 10;
jobData.b = 10;
jobData.result = result;

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
float aPlusB = result[0];

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

Unity C# Job System 2

  

The safety system in the C# Job system


Race conditions

멀티스레드 코드를 작성할때, 항상 Race Conditions 에 빠지는 것을 주의해야 한다. 이는 하나의 작업 결과값이 다른 스레드 영역에 있는 처리 결과가 완료될때까지 한없이 기다리게 되는 현상을 말한다.  이를 Race Conditions or Race Hazard라 한다.

이는 디버깅하기가 매우 힘들게 하고, 원인을 찾는 데 매우 어렵다. 그러므로 이런 코드를 작성하면 안된다.


Safety system

Job system은 이런 잠재적인 Race Conditions들을 탐지하고, 그들이 일으킬수도 있는 버그들을 막는다. 
예를 들어, 메인스레드에 있는 데이타의 reference를 Job System이 Job으로 전송했다면, 그 Job이 그 참조 데이타를 변경하려고 할때, 메인스레드에서 동시에 데이타를 읽을수도 있다. 이게 Race Condition 이다.  
 
C# Job System은 데이타의 reference가 아닌 복사본을 만들어서 Job에게 주는 것으로 이 문제를 해결한다.  

데이타 복사본은 Bittable data types 이어야 한다. 이는 Managed( Unity 로 작업한 사용자 코드) 와 Native Code (Unity Job System) 간의 컨버젼이 필요없는 호환 데이타 타입이다.  이 타입은 memcpy로 복사가 되고, 이 두 시스템간에 송수신이 가능하다. 


NativeContainer

데이타 복사처리의 단점은 이들 데이터들이 각 쓰레드 영역에서 분리되어 있다는 것이다. 이를 보완하기위해 나온것이 NativeContainer라 불리는 공유 메모리 타입이다. 


What is a NativeContainer?

이는 Managed value type 이다. Native 메모리를 안전하게  c# Wrapper를 가지고 있다. unmanaged 메모리 할당영역에 대한 포인터를 가지고 있다.  Unity C# Job System 사용시, 이는 Job이 메인스레드와 함께 접근할수 있도록 한다. 

What types of NativeContainer are available?

Unity는 NativeArray라 불리는 NativeContainer를 사용한다.  You can also manipulate a NativeArray with NativeSlice to get a subset of the NativeArray from a particular position to a certain length.

Note: ECS 패키지는 다음 타입들이 있는 Unity.Collections namespace 가지고 있다.
  • NativeList - a resizable NativeArray
  • NativeHashMap - key and value pairs
  • NativeMultiHashMap - multiple values per key
  • NativeQueue - a first in , first out (FIFO) queue

NativeContainer and the safety system

The safety system은 모두 NativeContainer 타입으로 이루어져 있다. 이것은 무엇이 읽고 있고, 어느 NativeContainer에 쓰고 있는지 추적한다.

Note: NativeContainer 타입의 안전 체크( out of bounds check, deallocation check, race condition check)는 UnityEditor, PlayMode 상에서만 작동합니다.

이 시스템은 DisposeSentinel, AtomicSafetyHandle 기술을 포함하고 있다. 
DisposeSentinel은 메모리 누수를 감지하고, 사용자의 메모리를 잘 해제안시키면, 에러를 발생시킨다. 
NativeContainer의 ownership을 코드상에서 전달해주고 싶을때는 AtomicSafetyHandle을 사용해라. 예를 들어, 두개의 Jobs들이 동시에 같은 NativeArray에 쓰기를 할때, 이 시스템은 에러를 출력하고 어떻게 해결할지를 알려준다. 이 경우에는 jobs들간의 의존성을 만들어서 해결 할수 있다. 첫번째 Job이 먼저 NativeContainer에게 쓰게하고 그 Job이 끝나면, 다음 Job이 안전하게 그것들을 읽고 같은 NativeContainer에게 쓰게 한다. 이런 쓰기 , 읽기 제한은 메인스레드에 있는 데이타를 접근할때도 적용된다. 이는 다수의 Jobs 들이 병렬로( 어떻게 이게 병렬이지? 병렬처럼 보이게 한다는 것인가?) 같은 데이타를 읽게 해준다. 

초기에, Job은 NativeContainer에 접근을 하면, 읽기, 쓰기 권한을 모두 갖는다. 이 설정은 성능에 않좋다.  이 시스템은 어떤 Job이 쓰고 있는 데이타에 동시에 또 다른 Job 이 쓰도록 권한을 주지 않는다.

만약 Job이 NativeContainer에게 쓰기권힌이 필요 없을때는 [ReadOnly] 를 선언시 붙여주자.

[ReadOnly]
public NativeArray<int> input;


Note: Job안에 있는 static 선언 데이타에 대해서는 이 안전시스템이 작동되지 않는다. Unity 가 죽는 요소일수 있으니 주의하기 바란다.

NativeContainer Allocator


NativeContainer를 만들때 반드시 필요한 메모리 할당 타입을 지정해야 한다. 이는 Job 을 수행하는 시간에 영향을 미친다. 각각의 상황별로 최고의 성능향상을 위해서는 이를 잘 지정해야한다.  아래와 같은 3가지 할당타입이 있다. 

  • Allocator.Temp
    • 가장 빠른 할당.
    • 1 frame 혹은 그보다 적은 수명을 갖는다. 
    • you should not pass NativeContainer allocations using Temp to Jobs.
    • Dispose 메서드를 호출해야 한다. ( Native 에서 Managed로 복귀할때)
  • Allocator.TempJob
    • Temp보다는 느리지만, Persistent 보다 빠른 할당.
    • 4 frame 의 수명. thread-safe.
    • 4 frame내에서 dispose를 호출하지 않으면, Warning이 뜬다. 
    • 대부분의 작은 Job들이 사용하는 타입이다.
  • Allocator.Persistent
    • 가장 느린 할당. 필요하다면 앱이 실행중인 내내 지속된다.
    • 작업량이 많은 Job일 경우 사용.
    • 성능이 필수적이라면 이 타입의 사용을 자제한다.

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

1은 NativeArray의 사이즈를 말한다. 이 경우 하나의 element를 갖는 array를 말한다.




Unity C# Job System

Job System

Unity에서 간단하고 안전하고, 성능좋은 멀티스레드를 수행하는 기술.
Burst compiler 와 함께 사용하면, 더 좋다.
Job system은 Unity내부적으로 돌아가는 Native Job system과 자연스럽게 통합된다. 그래서 유저가 작성한 Job System코드와 유니티가 worker thread를 공유한다.  이러한 협력이 cpu cores보다 더 많은 스레드를 만드는것을 막아준다. 이로 인한 cpu 자원을 낭비하는 것을 줄일수 있다.

What is Multithreading?

싱글스레드란, 하나의 작업을 한번에 수행하고, 결과가 이후에 나오는 것을 말한다. 멀티스레드는 다수의 cpu 의 장점을 이용한 프로그래밍 기술이다. 싱글스레드와는 달리 동시에 여러작업을 수행하게끔한다. 

프로그램시작시, 하나의 스레드가 기본적으로 실행된다. 이를 우린 Main Thread 라고 부른다. 그리고 그 이후에 여러 작업들을 하기위해 스레드들을 병렬로실행시킬수 있고 그 결과들을 Main Thread와 동기화시킬수 있다. 

작업량이 적다면, 몇개의 스레드로 원하는 바를 이룰수 있겠지만, 게임 개발에서는 많은 작업량들이 필요로 해진다. 이는 cpu와 OS 처리능력을 최대로 사용하게 된다. pool of Threads  라는 기술로 이를 보완할수 있지만, 이는 동시에 많은 활성화된 스레드들을 사용하게 되서, CPU자원을 소모하게 된다. 이는 잦은 context switching을 야기시키기에 성능에 않좋다. 

* Context Switching

하나의 스레드가 다른 스레드를 실행할때 현재 시점, 상태를 저장하고 멈춘다. 그리고 이후 실행된 스레드가 끝나면, 이전 스레드를 찾아가서 저장된 시점부터 다시 실행하게 하는데, 이를 Context Switching이라 부른다. 이는 cpu 자원소모가 큰 작업이다.  그래서 가급적 이를 피해야 한다.

What is a Job system?

Job system은 멀티스레드 코드를 스레드대신에 Jobs을 생성해서 운영한다. 멀티코어를 이용해 작업자 스레드들을 하나의 그룹으로 다룬다. 이는 보통 context switching을 피하기 위해 하나의 로직컬 cpu core당 하나의 작업자 스레드를 만든다. (비록 여분의 core가 남아 있다고 해도.)

Job system은 Jobs을 Job queue에 넣어서 실행한다. 작업자 스레드는 Job queue로부터 온 작업들을 수행한다. 정확한 순서로 작업하는지를 관리한다. 

What is a Job?

작업 하나를 의미한다.  Job은 변수들을 받고, 데이터들을 작업하고 ...
A job receives parameters and operates on data, similar to how a method call behaves. Jobs들은 독립적일수 있고 다른 Job들과 연계되어 실행될수도 있다.

What are job dependencies?

게임 개발같이 복잡한 시스템내에서는 각 Job들이 다른 Job들과 연계되는 경향이 많다. 하나의 Job이 보통 다음 Job 을위해 데이타를 준비한다. 이러한 의존성 작업들을 하기위해서 dependencies를 지원한다. 만약 JobA 가 JobB에 의존적이라면, JobA는 JobB가 끝날때까지는 시작하지 않는다.
 



출처