실시간 3D 콘텐츠를 개발할 때는 프레임이 원할하게 유지되어야 한다.
최소한 60FPS 이상은 유지가 되어야 하는데,
네트워크 작업이나 복잡한 계산을 지속적으로 처리해야 한다면,
그 마저도 불가능할 수 있다.
유니티는 기본적으로 메인 쓰레드(Main Thread) 중심 구조이고,
무거운 작업이 포함될 수록 프레임이 떨어진다.
반면에, 별도의 쓰레드를 만들어 무거운 작업을 떠넘긴다면,
최선의 성능을 이끌어낼 수 있다.
예시와 함께 사용법을 익혀보도록 하자.
① 무거운 작업 실행하기
일단, 의도적으로 프레임을 낮춰보자.
using UnityEngine;
public class Sample : MonoBehaviour
{
private void Update()
{
int sum = 0;
for (int i = 0; i < 1000000000; ++i) // 10억번 반복
++sum;
Debug.Log(sum); // 출력
}
}
Update 함수에서 for문을 10억번 반복하는 아주 무거운 코드이다.
이 코드 실행의 결과는 아래와 같다.
4.5FPS 이라는 엄청난 프레임 지연이 발생했다.
② 쓰레드에서 실행하기
위의 코드를 쓰레드에서 실행되도록 변환해보자.
using UnityEngine;
using System.Threading;
public class SampleThread : MonoBehaviour
{
private int? _sum = null;
private void Start()
{
Thread thread = new Thread(() => // 새 쓰레드 생성
{
while (true)
{
int sum = 0;
for (int i = 0; i < 1000000000; ++i)
++sum;
_sum = sum; // 메인 쓰레드에 접근할 수 없으므로, 결과를 필드 변수에 저장
}
});
thread.Start(); // 새 쓰레드 시작
}
private void Update()
{
if (_sum.HasValue) // 새 쓰레드가 저장한 값이 있으면 사용
{
Debug.Log(_sum);
_sum = null; // 값을 사용했으므로 null 처리
}
}
}
Thread 인스턴스를 생성 및 실행하는 것이 핵심이다.
인스턴스를 생성하면서 쓰레드 함수를 전달해야한다.
여기서 주의할 점은 쓰레드 내에서 메인 쓰레드의 함수를 실행할 수 없다는 것이다.
다만, 변수에는 접근할 수 있어서 필드 변수(_sum)에 값을 저장하고,
Update 함수에서 해당 변수에 값이 존재하면 사용하는 방식으로 구현할 수 있다.
이 코드는 높은 프레임을 유지하는 것을 볼 수 있다.
③ 쓰레드 중단하기
보통은 플래그 방식을 사용하여 쓰레드를 중단하는데,
특정 변수의 상태가 변경되면 취소가 되는 조건문을 추가하는 것이다.
using UnityEngine;
using System.Threading;
using System.Collections;
public class SampleThreadCancel : MonoBehaviour
{
private int? _sum = null;
private bool _cancel = false; // 취소 트리거를 발동하기 위한 변수
private IEnumerator Start()
{
new Thread(() =>
{
while (!_cancel) // 취소 명령이 없으면 계속 반복
{
int sum = 0;
for (int i = 0; i < 1000000000; ++i)
++sum;
_sum = sum;
}
}).Start();
yield return new WaitForSeconds(3f); // 3초 동안 대기
_cancel = true; // 변수를 통해 취소 명령
}
private void Update()
{
if (_sum.HasValue)
{
Debug.Log(_sum);
_sum = null;
}
}
}
While 문에 조건을 추가함으로써,
bool 타입의 변수가 TRUE가 되는 때에 쓰레드가 종료된다.
이 코드에서는 3초 뒤에 쓰레드가 종료된다.
※ 예전에는 쓰레드를 중단하기 위해서 Thread.Abort() 함수를 사용했으나,
이 방식은 강제로 중단시키는 것이라 예외가 발생하거나, 중단이 안될 수 있다.
④ 쓰레드 지연시키기
쓰레드는 Thread.Sleep(int millisecondsTimeout) 함수를 사용하여
ms 단위로 잠시 지연시킬 수 있다.
using UnityEngine;
using System.Threading;
using System.Diagnostics;
public class SampleThreadSleep : MonoBehaviour
{
private int? _sum = null;
private readonly int waitMs = 2000; // 2초마다 반복
private void Start()
{
new Thread(() =>
{
Stopwatch sw = new Stopwatch();
while (true)
{
sw.Restart();
int sum = 0;
for (int i = 0; i < 1000000000; ++i)
++sum;
_sum = sum;
if (sw.ElapsedMilliseconds < waitMs) // 위 계산에 걸린 시간이 0.5초면
Thread.Sleep(waitMs - (int)sw.ElapsedMilliseconds); // 1.5초(2초 - 0.5초) 대기
}
}).Start();
}
private void Update()
{
if (_sum.HasValue)
{
UnityEngine.Debug.Log(_sum);
_sum = null;
}
}
}
이 코드에서는 2초에 한번씩 필드 변수에 값을 할당하도록 했다.
특이한 것은 Stopwatch 클래스로 for 문의 시간을 측정했다는 것인데,
복잡한 계산에 걸린 시간이 2초가 넘어가면 기다릴 필요가 없기 때문이다.
만약, 2초 이하의 시간이 걸렸다면 남은 시간만큼만 기다려주면 된다.
+ Task 방식으로 실행하기
대부분의 경우에는 Thread 보다는 Task를 활용하는 것이 빠르고 효율적이라고 한다.
따라서, 위 코드들을 통합하여 Task 버전으로 변환해보겠다.
어렵지 않으므로 다른 부분에만 주석 처리했다.
using UnityEngine;
using System.Threading;
using System.Threading.Tasks;
using System.Collections;
using System.Diagnostics;
public class SampleTask : MonoBehaviour
{
private int? _sum = null;
private bool _cancel = false;
private readonly int waitMs = 2000;
private IEnumerator Start()
{
Task task = new Task(() => // Thread와 유사하게 함수를 전달
{
Stopwatch sw = new Stopwatch();
while (!_cancel)
{
int sum = 0;
for (int i = 0; i < 1000000000; ++i)
++sum;
_sum = sum;
if (sw.ElapsedMilliseconds < waitMs)
Thread.Sleep(waitMs - (int)sw.ElapsedMilliseconds);
}
});
task.Start(); // 또는 Task.Run() 함수로 단일 실행 가능
yield return new WaitForSeconds(10f);
_cancel = true;
}
private void Update()
{
if (_sum.HasValue)
{
UnityEngine.Debug.Log(_sum);
_sum = null;
}
}
}
쓰레드는 프로그램의 성능을 극대화 시킬 수 있는 강력한 무기임은 맞지만,
CPU에 부담을 줄 만큼 남발해서는 안된다.
또한, 쓰레드들간의 통신을 정확하게 해내지 못하면,
예상치 못한 예외를 발생시킬 수 있으므로, 섬세한 작업이 요구된다.
마지막으로 글을 작성하는 시점까지도 UnityWeb(구 WebGL)에서는
쓰레드를 지원하지 않으므로 참고하자.
'[Unity]' 카테고리의 다른 글
[Unity] URP 스텐실(Stencil) 활용하기 (1) | 2024.09.05 |
---|---|
[Unity] Input System으로 입력 처리하기 (35) | 2024.06.08 |
[Unity] 오브젝트 풀링을 간단히 구현해보자(ObjectPool) (38) | 2024.05.24 |
[Unity] 코루틴을 대체하자(UniTask) (43) | 2024.05.15 |
[Unity] 선을 그리는 UI를 만들어보자 (60) | 2024.04.28 |
댓글