반응형
비동기 프로그래밍(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 키워드가 메서드의 실행을 일시 중지시킨다고 해도, 전체 프로그램 실행을 중지시키지 ❌
awaitasync 메서드 내에서만 사용 가능

[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가 완료될 때까지 대기하지 않습니다.

WhenAny()
Task[] tasks = { Task.Run(() => DoSomething()), Task.Run(() => DoSomethingElse()) };
Task completedTask = await Task.WhenAny(tasks);  // 첫 번째로 완료된 Task를 반환​​
비동기적으로 실행되며, 전달된 Task 중 하나가 완료될 때까지 기다린 후, 그 완료된 Task를 반환합니다.
await를 통해 비동기적으로 대기하기 때문에, 리소스 낭비가 적고 효율적입니다.

 


WaitAll() / WaitAny()동기적이며 현재 스레드를 차단하지만, WhenAll() / WhenAny()비동기적으로 처리하여 CPU 리소스를 더 효율적으로 사용합니다.

 

4. Wait() vs await

wait()

await

 

 

[C#] Task .Wait() vs await 차이점

Task.Wait과 await의 차이점 Stack Overflow에서 발견한 흥미로운 질문과 답변입니다. set 출처: https://stackoverflow.com/questions/9519414/whats-the-difference-between-task-start-wait-and-async-await kayuse88.github.io C# await - C# 프

cypsw.tistory.com

 

4. async/await, 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 시스템은 이미 최적화되어 있어 대부분의 경우 동기적으로 처리해도 문제가 없습니다.

더 적절한 사용 사례는 다음과 같습니다:

  1. 커스텀 경로 찾기 알고리즘 사용 시
  2. 매우 큰 규모의 월드에서의 경로 계산
  3. 동시에 많은 AI 엔티티의 경로를 계산해야 할 때
반응형

+ Recent posts