비동기 프로그래밍(async/await 연산자)의 주요 특징 1. System.Threading.Tasks 네임스페이스 사용 2. 비차단 실행: 시간이 오래 걸리는 작업을 실행하면서도 메인 스레드를 차단하지 않습니다. 3. 병렬 처리: 여러 작업을 동시에 실행할 수 있습니다. 4. 리소스 효율성: CPU와 메모리를 더 효율적으로 사용할 수 있습니다. 5. 확장성: 동시에 많은 작업을 처리할 수 있어 시스템의 확장성이 향상됩니다. 6. 반응성: UI 응답성을 유지하면서 백그라운드 작업을 수행할 수 있습니다.
1. 동기/비동기 이해 [동기(Synchronous)] 한 가지 작업이 완료되어야만 다음 작업이 시작되는 방식! 동기 프로그래밍 [비동기(Asynchronous)] 한 가지 작업이 완료되지 않아도 다음 작업이 시작될 수 있는 방식 비동기 프로그래밍
2. async, await, Task 상세 설명 [async 키워드] • 비동기 메서드를 선언할 때 사용하는 키워드 • 메서드, 람다 표현식, 또는 익명 메서드가 비동기적임을 나타냄 • 변환 타입으로 Task, Task<T>, 또는 void(이벤트 핸들러에서만 권장)를 사용 [await 키워드] • 비동기 메서드 내에서 사용되는 키워드 • 비동기 작업의 완료될 때까지 현재 메서드의 실행을 일시 중지하고, 비동기 작업이 완료되면 메서드의 실행을 계속 진행 • await 키워드가 메서드의 실행을 일시 중지시킨다고 해도, 전체 프로그램 실행을 중지시키지 ❌ • await는 async 메서드 내에서만 사용 가능
[Task 클래스] • 비동기 작업을 나타냄 • Task는 반환값이 없는 작업, Task<T>는 T타입의 결과를 반환하는 작업 • Task.Run()을 사용하여 새 작업을 시작 가능 • Task.WhenAll()과 Task.WhenAny()를 사용하여 여러 작업 조율 가능
Q. WhenAll(), WhenAny()와 WaitAll(), WaitAny()의 차이는? 스레드의 코드 실행을 막는지 여부! When 메서드는 Task 객체를 반환하기 때문에 await 키워드로 비동기 흐름을 만들 수 있지만, Wait 메서드는 작업이 완료되길 기다려야 한다. 자세한건 아래 '4.' 참고!
1. WaitAll() vs WhenAll() WaitAll():
Task[] tasks = { Task.Run(() => DoSomething()), Task.Run(() => DoSomethingElse()) };
Task.WaitAll(tasks); // 모든 Task가 완료될 때까지 대기 (동기적)
• 동기적으로 실행되며, 전달된 모든 Task가 완료될 때까지현재 스레드를 차단합니다. • 모든 작업이 완료될 때까지 다음 코드로 진행하지 않기 때문에, CPU 리소스를 활용하지 못하고 블로킹됩니다.
WhenAll():
Task[] tasks = { Task.Run(() => DoSomething()), Task.Run(() => DoSomethingElse()) };
await Task.WhenAll(tasks); // 모든 Task가 완료될 때까지 비동기 대기
• 여러 작업을 묶어서 하나의 작업으로 만들고묶인 작업들이 다 끝나야 완료 • await를 통해 비동기 작업이 끝날 때까지 대기하므로, CPU 리소스를 블로킹하지 않고 효율적으로 사용할 수 있습니다.
2. WhenAny() vs WhenAny() WaitAny():
Task[] tasks = { Task.Run(() => DoSomething()), Task.Run(() => DoSomethingElse()) };
int completedTaskIndex = Task.WaitAny(tasks); // 첫 번째로 완료된 Task의 인덱스를 반환
• 동기적으로 실행되며, 전달된 Task중 하나가 완료될 때까지현재 스레드를 차단합니다. • 완료된 Task의 인덱스를 반환하며, 다른 Task가 완료될 때까지 대기하지 않습니다.
public class AssetLoader : MonoBehaviour
{
// 1. 텍스처를 비동기적으로 로드하는 메서드
public async Task<Texture2D> LoadTextureAsync(string path)
{
// 2. UnityWebRequest를 사용하여 텍스처 다운로드 요청
UnityWebRequest www = UnityWebRequestTexture.GetTexture(path);
// 3. 요청을 비동기적으로 전송하고 완료될 때까지 대기
await www.SendWebRequest();
// 4. 요청 결과 확인
if (www.result != UnityWebRequest.Result.Success)
{
Debug.LogError(www.error);
return null;
}
// 5. 다운로드된 텍스처 반환
return DownloadHandlerTexture.GetContent(www);
}
// 6. 여러 게임 에셋을 동시에 로드하는 메서드
public async void LoadGameAssets()
{
try
{
// 7. 두 개의 텍스처 로딩 작업 시작
Task<Texture2D> characterTask = LoadTextureAsync("Characters/hero.png");
Task<Texture2D> backgroundTask = LoadTextureAsync("Backgrounds/level1.png");
// 8. 두 작업이 모두 완료될 때까지 대기
await Task.WhenAll(characterTask, backgroundTask);
// 9. 로드된 텍스처 사용
characterRenderer.material.mainTexture = characterTask.Result;
backgroundRenderer.material.mainTexture = backgroundTask.Result;
Debug.Log("All assets loaded successfully!");
}
catch (Exception e)
{
// 10. 예외 처리
Debug.LogError($"Asset loading failed: {e.Message}");
}
}
}
[네트워크 통신 (플레이어 데이터 저장)]
public class PlayerDataManager : MonoBehaviour
{
private const string API_URL = "https://api.example.com/saveplayerdata";
// 1. 플레이어 데이터를 비동기적으로 저장하는 메서드
public async Task SavePlayerDataAsync(PlayerData data)
{
// 2. 플레이어 데이터를 JSON으로 변환
string json = JsonUtility.ToJson(data);
// 3. POST 요청 생성
using (UnityWebRequest www = UnityWebRequest.Post(API_URL, json))
{
// 4. 요청 헤더 설정
www.SetRequestHeader("Content-Type", "application/json");
try
{
// 5. 요청을 비동기적으로 전송하고 완료될 때까지 대기
await www.SendWebRequest();
// 6. 요청 결과 확인
if (www.result != UnityWebRequest.Result.Success)
{
Debug.LogError($"Data upload failed: {www.error}");
}
else
{
Debug.Log("Player data saved successfully!");
}
}
catch (Exception e)
{
// 7. 네트워크 오류 처리
Debug.LogError($"Network error: {e.Message}");
}
}
}
// 8. 플레이어 레벨업 시 호출되는 메서드
public async void OnPlayerLevelUp(int newLevel)
{
// 9. 새로운 플레이어 데이터 생성
PlayerData data = new PlayerData
{
level = newLevel,
timestamp = DateTime.Now.ToString()
};
// 10. 플레이어 데이터 저장 메서드 호출
await SavePlayerDataAsync(data);
}
}
[AI 경로 찾기]
public class AIPathfinder : MonoBehaviour
{
private NavMeshAgent agent;
// 1. 컴포넌트 초기화
private void Start()
{
agent = GetComponent<NavMeshAgent>();
}
// 2. 비동기적으로 경로를 계산하는 메서드
public async Task<NavMeshPath> CalculatePathAsync(Vector3 destination)
{
NavMeshPath path = new NavMeshPath();
// 3. 경로 계산을 별도의 스레드에서 실행
await Task.Run(() =>
{
NavMesh.CalculatePath(transform.position, destination, NavMesh.AllAreas, path);
});
// 4. 계산된 경로 반환
return path;
}
// 5. AI를 목적지로 이동시키는 메서드
public async void MoveToDestination(Vector3 destination)
{
try
{
// 6. 비동기적으로 경로 계산
NavMeshPath path = await CalculatePathAsync(destination);
// 7. 경로의 유효성 확인
if (path.status == NavMeshPathStatus.PathComplete)
{
// 8. NavMeshAgent에 경로 설정
agent.SetPath(path);
Debug.Log("AI is moving to the destination.");
}
else
{
Debug.LogWarning("Cannot find a complete path to the destination.");
}
}
catch (Exception e)
{
// 9. 예외 처리
Debug.LogError($"Pathfinding error: {e.Message}");
}
}
}
Q. 3번(AI 경로 찾기)에서 비동기 프로그래밍을 사용하는 이유? • 복잡한 계산: - 대규모 게임 월드나 복잡한 환경에서 경로 찾기는 계산 비용 🔺 - 특히 A* 알고리즘 같은 고급 경로 찾기 알고리즘은 시간이 🔺 •프레임 레이트 유지: - 경로 계산을 메인 스레드에서 동기적으로 수행하면 게임의 프레임 레이트가 🔻 - 비동기 처리를 통해 메인 게임 루프의 중단을 방지 ⭕️ •다수의 AI 엔티티: - 많은 AI 캐릭터가 동시에 경로를 계산해야 하는 경우, 비동기 처리가 유용 •반응성 향상: - 플레이어의 입력에 즉시 반응하면서도 백그라운드에서 경로 계산 ⭕️ • 최적화 가능성: - 비동기 처리를 통해 여러 경로 계산을 병렬로 수행 ⭕️
다만, 주어진 예시 코드에서는 간단한 NavMesh 경로 계산을 수행하고 있어, 이 정도 수준에서는비동기 처리가 과도할 수 있습니다. 실제로 Unity의 NavMesh 시스템은 이미 최적화되어 있어 대부분의 경우 동기적으로 처리해도 문제가 없습니다.