오늘은 UniTask에 대해 설명해보는 시간을 가지겠습니다.
| 특징 | Task | UniTask |
|---|---|---|
| API 표준성 | .NET 표준 기반, Unity 외부에서도 사용 가능 | Unity 특화, Unity 외부 사용에는 부적합 |
| 성능 (GC 비용) | 생성 및 실행 시 GC 비용이 큼 | GC 비용 최소화 |
| Unity 메인 스레드 지원 | 메인 스레드 접근 시 추가 작업 필요 | Unity API와 자연스럽게 통합 |
| 프레임 제어 | Unity 프레임 제어가 복잡함 | Unity 프레임 기반 비동기 작업 지원 |
| 사용 난이도 | C# 기본 비동기 문법으로 사용 간편 | 추가 학습이 필요, Unity 개발에 적합 |
| 멀티스레드 지원 | 스레드 기반 작업 가능 | 스레드 지원은 제한적 |
| 호환성 | 다양한 .NET 라이브러리와 호환 | Unity 외 프로젝트와는 호환성 부족 |
public class TaskGCTest : MonoBehaviour
{
private const int TaskCount = 100;
private async void Start()
{
Debug.Log("Task 테스트 시작");
int gcBefore = System.GC.CollectionCount(0);
long memoryBefore = System.GC.GetTotalMemory(false);
for (int i = 0; i < TaskCount; i++)
{
await Task.Run(() =>
{
int result = 0;
for (int j = 0; j < 10; j++)
{
result += j;
}
});
}
int gcAfter = System.GC.CollectionCount(0);
long memoryAfter = System.GC.GetTotalMemory(false);
Debug.Log($"Task: 메모리 할당량 = {memoryAfter - memoryBefore} bytes");
Debug.Log($"GC Collection 횟수: {gcAfter - gcBefore}");
}
}
public class UniTaskGCTest : MonoBehaviour
{
private const int TaskCount = 100;
private async void Start()
{
Debug.Log("UniTask 테스트 시작");
int gcBefore = System.GC.CollectionCount(0);
long memoryBefore = System.GC.GetTotalMemory(false);
for (int i = 0; i < TaskCount; i++)
{
await UniTask.Create(async () =>
{
int result = 0;
for (int j = 0; j < 10; j++)
{
result += j;
}
});
}
int gcAfter = System.GC.CollectionCount(0);
long memoryAfter = System.GC.GetTotalMemory(false);
Debug.Log($"UniTask: 메모리 할당량 = {memoryAfter - memoryBefore} bytes");
Debug.Log($"GC Collection 횟수: {gcAfter - gcBefore}");
}
}




UniTask를 사용하면 굉장히 많이 줄어든다.
문제는 RunOnThreadPool 을 사용하여 테스트를 처음 구현하였는데 이 때는 UniTask가 더 GC 비용이 많이 나왔다...
해당 문제 발생 원인은 Task의 경우 .NET ThreadPool을 사용하여
이미 할당된 스레드를 재사용하므로 추가적인 메모리 할당이 최소화 되고
오랜 시간 최적화 된 스레드 풀 관리 시스템을 가지고 있다고 하고
UniTask의 경우에는 Unity 환경에 맞춘 ThreadPool을 사용한다고 합니다.
이러면 가볍고 빠르다는 장점이 없어지는거 아닌가 싶긴합니다..
일단 그래서 Create로 변경하여 사용하였습니다.
결론적으로는 UnityAPI와 무관한 CPU 작업인 경우에는 Task
연관된 작업이면 UniTask 사용이 권장된다고 합니다..
| 메서드 | 설명 | 사용 사례 |
|---|---|---|
| UniTask.Delay | 일정 시간 동안 대기 | 비동기 대기 처리 (Coroutine의 WaitForSecond..) |
| UniTask.DelayFrame | 특정 프레임 수만큼 대기 | Unity의 프레임 기반 대기 처리 |
| UniTask.Yield | 다음 프레임까지 대기 | 간단한 한 프레임 대기 처리 |
| UniTask.WhenAll | 여러 비동기 작업을 병렬로 실행하고 모든 작업이 완료될 때까지의 대기 | 다중 비동기 작업의 병렬 처리 |
| UniTask.WhenAny | 여러 비동기 작업 중 하나라도 완료될 때까지 대기 | 첫번째 완료되는 비동기 작업 처리 |
| UniTask.RunOnTheradPool | 백그라운드 스레드 풀에서 작업 실행 | CPU 작업 또는 Unity 메인 스레드와 무관한 작업 처리 |
| UniTask.Run | 비동기 작업 실행 (이제 사용 안함 -> RunOnThreadPool 사용) | 간단한 비 동기 작업 실행 |
| UniTask.Create | 새로운 비동기 작업을 생성 | 간단한 비동기 작업 작성 |
| UniTask.SwitchToMainThread | 작업을 Unity 메인 스레드로 전환 | Unity API 호출이 필요한 작업 처리 |
| UniTask.SwitchToThreadPool | 작업을 스레드 풀로 전환 | CPU 처리 |
private async void Start()
{
Debug.Log("3초 대기 시작...");
await UniTask.Delay(3000);
Debug.Log("3초 대기 완료!");
}
private async void Start()
{
Debug.Log("5 프레임 대기 시작...");
await UniTask.DelayFrame(5);
Debug.Log("5 프레임 대기 완료!");
}
private async void Start()
{
Debug.Log("다음 프레임까지 대기 시작...");
await UniTask.Yield();
Debug.Log("다음 프레임으로 이동 완료!");
}
private async void Start()
{
Debug.Log("병렬 작업 시작...");
await UniTask.WhenAll(TaskA(), TaskB());
Debug.Log("모든 작업 완료!");
}
private async UniTask TaskA()
{
await UniTask.Delay(2000);
Debug.Log("작업 A 완료!");
}
private async UniTask TaskB()
{
await UniTask.Delay(3000);
Debug.Log("작업 B 완료!");
}
private async void Start()
{
Debug.Log("최초 완료 작업 대기...");
await UniTask.WhenAny(TaskA(), TaskB());
Debug.Log("최초 작업 완료!");
}
private async UniTask TaskA()
{
await UniTask.Delay(2000);
Debug.Log("작업 A 완료!");
}
private async UniTask TaskB()
{
await UniTask.Delay(3000);
Debug.Log("작업 B 완료!");
}
private async void Start()
{
Debug.Log("백그라운드 작업 시작...");
await UniTask.RunOnThreadPool(() =>
{
int result = 0;
for (int i = 0; i < 1000000; i++) result += i;
Debug.Log($"계산 결과: {result}");
});
Debug.Log("백그라운드 작업 완료!");
}
private async void Start()
{
await UniTask.RunOnThreadPool(() =>
{
Debug.Log("백그라운드에서 작업 수행 중...");
});
await UniTask.SwitchToMainThread();
Debug.Log("메인 스레드로 전환 완료!");
}
private async void Start()
{
Debug.Log("메인 스레드 작업 중...");
// 백그라운드 스레드로 전환
await UniTask.SwitchToThreadPool();
Debug.Log("백그라운드 스레드 작업 수행 중...");
}
private async void Start()
{
await UniTask.Create(async () =>
{
Debug.Log("사용자 정의 작업 실행 중...");
await UniTask.Delay(1000);
Debug.Log("사용자 정의 작업 완료!");
});
}
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
public class AsyncImageLoader : MonoBehaviour
{
public Image targetSpriteRenderer;
public Sprite defaultSprite;
public string imageName = "22"; // 어드레서블을 사용하는 경우에는 어드레서블 경로를 사용하면 좋을 듯 합니다
public int delay = 0;
public bool bNetworkSuccess;
private async void Start()
{
SetDefaultImage();
await LoadImageBasedOnNetworkStatus();
}
private void SetDefaultImage()
{
if (targetSpriteRenderer != null && defaultSprite != null)
{
targetSpriteRenderer.sprite = defaultSprite;
Debug.Log("기본 이미지가 설정되었습니다.");
}
else
{
Debug.LogError("SpriteRenderer 또는 기본 이미지가 설정되지 않았습니다.");
}
}
private async UniTask LoadImageBasedOnNetworkStatus()
{
try
{
// 네트워크 상태 확인 (현재는 그냥 변수로 테스트)
bool isNetworkConnected = await CheckNetworkConnectionAsync();
if (!isNetworkConnected)
{
Debug.LogWarning("네트워크 연결이 끊어져 있습니다. 기본 이미지로 유지됩니다.");
return;
}
// 비동기로 이미지 로드
Sprite loadedSprite = await LoadImageFromResourcesAsync(imageName);
// Unity 메인 스레드에서 SpriteRenderer에 이미지 설정
await UniTask.SwitchToMainThread();
if (targetSpriteRenderer != null && loadedSprite != null)
{
targetSpriteRenderer.sprite = loadedSprite;
Debug.Log("네트워크 이미지를 성공적으로 로드하고 설정했습니다.");
}
}
catch (System.Exception ex)
{
Debug.LogError($"이미지 로드 실패: {ex.Message}");
}
}
private async UniTask<bool> CheckNetworkConnectionAsync()
{
await UniTask.Delay(delay);
#region 네트워크 HTTP 요청
// 네트워크 연결 상태를 확인하기 위해 HTTP 요청
//try
//{
// using (UnityWebRequest request = UnityWebRequest.Get(testUrl))
// {
// request.timeout = 5;
// await request.SendWebRequest();
// if (request.result == UnityWebRequest.Result.Success)
// {
// Debug.Log("네트워크 연결 확인 성공");
// return true; // 연결 성공
// }
// else
// {
// Debug.LogWarning($"네트워크 연결 확인 실패: {request.error}");
// return false; // 연결 실패
// }
// }
//}
//catch (System.Exception ex)
//{
// Debug.LogError($"네트워크 확인 중 예외 발생: {ex.Message}");
// return false;
//}
#endregion
// 지금은 그냥 변수로 체크
return bNetworkSuccess;
}
private async UniTask<Sprite> LoadImageFromResourcesAsync(string resourceName)
{
// 메인 스레드로 전환 -> Resource.Load 접근을 위한
await UniTask.SwitchToMainThread();
Debug.Log($"Resources에서 이미지 로드 중... ({resourceName})");
// 메인 스레드에서 `Resources.Load` 호출
Sprite sprite = Resources.Load<Sprite>(resourceName);
if (sprite == null)
{
throw new System.Exception($"Resources에서 '{resourceName}'를 찾을 수 없습니다.");
}
return sprite;
}
}





이미지 1과 3은 bool 값이 true라 성공적으로 이미지가 변동된 모습이고
이미지 2는 bool 값이 false라 기본 이미지로 보입니다.
UniTask가 생각보다 Task보다 그렇게 좋은건 아니라는 걸 느꼈고 지금은 겉핥기 식으로 해서 사용법이 쉬웠지만 좀 더 복잡한 작업을 해야할 때에는 연동이 어려울 것으로 느껴졌습니다. 이를 위해 UniTask도 공부를 많이 해놔야 될 것 같은 필요성을 느낍니다..