먼저 코루틴에 대해서 간략하게 알아보자!
코루틴 단어 그대로의 직역은 Co(함께, 서로) + routine(규칙적 일의 순서, 작업의 집합) 2개가 합쳐진 단어로 함께 동작하며 규칙이 있는 일의 순서를 뜻한다.
코루틴 : 비선점적 멀티태스킹을 위한 서브 루틴을 일반화한 컴퓨터 프로그램 구성요소
비선점적 멀티태스킹이란?
- 비선점형 : 하나의 프로세스가 CPU를 할당받으면 종료되기 전까지 다른 프로세스가 CPU를 강제로 차지할 수 없습니다. (코루틴)
- 선점형 : 하나의 프로세스가 다른 프로세스 대신에 프로세서(CPU)를 강제로 차지할 수 있습니다. (쓰레드)
따라서 코루틴은 병행성은 제공하지만, 병렬성은 제공하지 않습니다.
- 병행성(=동시성, Concurrency) : 논리적으로 병렬로 작업이 실행되는 것처럼 보이는 것
- 병렬성(Parallelism) : 물리적으로 병렬로 작업이 실행되는 것
선점형으로 비동기 처리 시에는 하기와 같은 몇 가지 문제점이 있었습니다.
- 코드의 복잡성 : 독립적인 쓰레드 안에서 각 루틴이 동작하기에 동시성을 제어하는 복잡한 코드가 필요하며 이는 코드 흐름 파악을 어렵게 합니다.
- 비용 : 무분별하게 쓰레드를 사용하면 Context switching 리소스 비용으로 프로그램 성능이 저하됩니다.
이러한 문제를 해결하기 위해 다양한 방법이 나오는데, 코루틴은 이런 단점을 해결해 주는 언어적 지원 방법이며 비선점적 멀티태스킹을 사용하여 해결하고 있습니다.
코루틴은 쓰레드가 아닌 쓰레드 내 동작하는 하나의 작업 단위이며 정의된 다양한 구성요소의 집합인 Context를 오버라이드 하며 실행됩니다. 따라서 쓰레드 내 Context switching 없이 여러 코루틴을 실행, 중단, 재개하는 상호작용을 통해 병행성을 갖기에 쓰레드와 메모리 사용이 줄어들고 개발자가 직접 작업을 스케줄링 할 수 있도록 합니다.
우리는 게임제작을 할 때 Coroutine을 사용하면서 시간 제어를 하기위해 yield return new waitforseconds(); 라는 것을 사용할 것이다.
하지만 여기서 yield 구문과 함께 new 키워드를 사용하게 되면 인스턴스가 일어나 GC가 발생하는 원인이 된다.
private IEnumerator 대기(){
yield return new waitforseconds(1f);
}
이런식으로 코루틴으로 시간 제어를 할 것이다. 위 코드 같은경우 한번만 사용하는 것이면 상관이 없다. 하지만 아래와 같이 여러번 사용하는 코드라면 지속적으로 인스턴스가 되면서 최적화 이슈가 생길 수 있다.
private IEnumerator 대기(){
for(int i=0; i<100; i++){
yield return new waitforseconds(1f);
}
}
그래서 보통 캐싱을 해두고 사용하는 방법이 좋은 방법이다.
private IEnumerator 대기(){
waitforseconds 1초대기 = new waitforseconds(1f); //캐싱
for(int i=0; i<100; i++){
yield return 1초대기; //캐싱한 변수 사용
}
}
이러한 방법으로 캐싱을 하면서 new 를 아낄 수 있다.
그러나 다른 여러 클래스에서도 비슷한 값을 캐싱하는 경우(중복)가 있었고, 그때 마다 계속해서 캐싱을 해주어야하는 불편함이 생긴다. 이러한 불편함을 없애기 위해 여러 클래스에서 캐싱한 값을 사용하는 static class를 하나 만들어주겠다.
WaitForEndOfFrame, WaitForFixedUpdate 같은 경우는 float 값에 따라 변동하지 않기에 하나만 생성해두고 사용한다.
Static Class YieldCache{ public static readonly WaitForEndOfFrame CacheWaitForEndOfFrame = new WaitForEndOfFrame();
public static readonly WaitForFixedUpdate CacheWaitForFixedUpdate = new WaitForFixedUpdate();
}
이러한 방식으로 여러 클래스에서 사용하면 편할 것이다.
다음은 WaitForSeconds를 캐싱하는 방법인데 float타입을 key, WaitForSeconds 타입을 value로 설정한 Dictionary를 사용하는 형태이다.
private static readonly Dictionary<float, WaitForSeconds> timeInterval = new Dictionary<float, WaitForSeconds>();
public static WaitForSeconds WaitForSeconds(float seconds){
WaitForSeconds wait;
if(timeInteval.TryGetValue(seconds, out wait) == false)
timeInterval.Add(seconds, wait = new WaitForSeconds(seconds));
return wait;
}
이제 선언해둔 Dictionary를 `WaitForSeconds()` 함수에서 이미 캐싱된게 있으면 불러와서(`TryGetValue`) 넣어주고 없다면 새로 넣어준 후 리턴해주는 효율적인 구조가 완성된다.
추가로
Dictionary의 Value값을 찾을 때 ContainKey() 보다 TryGetValue()를 사용하는 것이 좋다. 값을 참조하는 경우 TryGetValue가 더 빠르고, 단순하게 Key 존재 여부를 파악하는 것에도 큰 차이가 없기도하니 웬만해서는 TryGetValue가 좋다.
사용
private IEnumerator 대기(){
for(int i=0; i<100; i++){
yield return WaitCache.WaitForSeconds(1f); //스태틱 캐싱 변수 사용
}
}
결국
원하는 곳에서 기존처럼 사용할 때마다 일일이 캐싱해두는 것 보다 훨씬 간편하고 효율적인 구조가 되었다.
그럼 바로 사용해보러 가자!!!!!
'유니티' 카테고리의 다른 글
FSM, Behavior Tree - Unity (0) | 2024.07.11 |
---|---|
유니티 - Mirror 셋팅 및 Attribute 간단 설명 (0) | 2024.06.09 |
유니티 최적화 - Engine (0) | 2024.05.10 |
유니티 최적화 - Garbage Collection (0) | 2024.05.09 |
UniTask (0) | 2024.04.30 |