개념공부

Task, Async/Await

Cadi 2025. 2. 22. 04:38

Task 

 

Task

  • 비동기 프로그래밍(Asynchronous Programming)을 쉽게 구현할 수 있도록 도와주는 C#의 기능Task 
  • 메인 스레드를 블로킹하지 않고 코드 실행을 계속할 수 있음
  • Unity에서 메인 스레드를 유지하면서도 백그라운드 작업을 수행할 때 유용
  • 스레드 및 스레드풀의 단점을 개선하면서 나옴
using System.Threading.Tasks;

 

Threading.Tasks namespace를 통해 활용할 수 있다. 

 

using System;
using System.Diagnostics;
using System.Threading.Tasks;

public class TestTask
{
    public static void Main()
    {
        Task task = new Task(SleepAction);
        task.Start();
        task.Wait();
        Console.WriteLine("Task finished");
        void SleepAction()
        {
            Thread.Sleep(1000);
        }
    }
   
}

 

다음과 같이 코드를 진행하면 task가 완료될 때까지 (1초) 콘솔창에 Task finished가 뜨지 않는다. 

 

Wait까지는 thread.Join과 비슷하지만 더욱 발전된 함수들도 있다.

 

  • Task.Run : 생성과 실행을 한 번에 하는 메서드
    * 백그라운드 스레드에서 실행됨(ThreadPool 활용)
  • task.ContinueWith() : Task가 완료될 후 수행할 작업 지정
  • WhenAll (,) 매개변수로 받은 Task들이 모두 완료된 후 실행되는 메서드
  • WhenAny(,) : 매개변수로 받은 Task들 중 하나라도 완료되면 IsCompleted로 상태가 바뀌는 매서드
  •  Task.Factory.StartNew : Run보다 조금 더 많은 옵션을 넣을 수 있는 메서드, 사용법이 복잡

 

* cancellationTOken이나 스케쥴러도 있다. 

using System;
using System.Diagnostics;
using System.Threading.Tasks;

public class TestTask
{
    public static void Main()
    {

        Task<int> task1 = new Task<int>(() => 1);
        Task<float> task2 = Task.Run(() => 2f);
        Task<double> task3 = new Task<double>(() => 3d);
        
        
        task1.Start();
        task3.Start();
        
        Console.WriteLine(task1.Result);
        Console.WriteLine(task2.Result);
        Console.WriteLine(task3.Result);
    }
   
}

 

다음과 같이 리턴값을 받을 수도 있다. 

*중간에 Run을 쓰면 Start를 따로 해주지 않아도 된다. 

 

 


02. Async Await

 

동기 실행 : 작업이 순차적으로 일어나는 방식

출처 : https://blog.naver.com/i_am_gamer/223504636255

비동기 방식 :프로그램이 병렬적으로 실행되도록 하여 작업을 효율적으로 처리할 수 있게 해줌

                     주로 I/O 작업 ( 파일 읽기/쓰기 , 네트워크 통신) 에 사용

출처 : https://blog.naver.com/i_am_gamer/223504636255

 

 

*C# 1.0때는 굉장히 복잡한 방식으로 했었다.

 

이런 복잡함을 개선하기 위해서 나온 개념이 Async / Await 

 

사용 방식

 

using System;
using System.Threading.Tasks;

public class TestTask
{
    public static async Task Main()
    {
        await SleepAsync();  // ✅ 메인 스레드를 차단하지 않고 1초 기다림
        Console.WriteLine("Task finished");
    }

    private static async Task SleepAsync()
    {
        await Task.Delay(1000);  // ✅ 현재 스레드는 계속 실행되며 1초 후에 다시 실행됨
    }
}

 

매개변수 앞에 반환 타입으로 Task ( 혹은 Task<TResult>) 를 받아준다.

그리도 비동기로 실행될 코드 앞에 await 키워드를 붙여주면 된다. 

awiat 키워드는 async로 선언된 함수 안에서만 동작한다. 

 

Unity에서 async / await를 써야 하는 이유

 

의문 : 굳이 async / await 방식을 사용해야 하는 이유가 있을까 ? 

 

Task task = new Task(SleepAction);
task.Start();
task.Wait();  // ✅ 메인 스레드가 여기서 멈춤
Console.WriteLine("Task finished");

void SleepAction()
{
    Thread.Sleep(1000);  // ✅ 현재 실행 중인 스레드를 1초 동안 차단
}

 

이런 식으로 호출하는 것과의 차이점은 뭘까 ? 

 

async/ await 방식은  ' 메인 스레드를 차단하지 않는다' 는 장점이 있다.

task.Wait() 는 실행중인 메인 스레드를 블로킹하고 , task가 완료될 때까지 기다리지만,

await Task.Delay(1000); 은 메인 스레드를 블록하지 않고 기다렸다가 실행을 재개한다.

 

예를 들어, UI가 있는 애플리케이션이고, 이 UI가 계속해서 Update문에서 움직이는 등의 동작이 있다면,

Wait()를 사용하면 멈춰버릴 수도 있다. 

 

두 번쨰 장점은 '많은 작업을 동시에 실행 가능'하다는 점이다. 

위에서 알아봤던 Task.WhenAll(,) 과 같은 메서드를 통해 두 개의 작업을 동시해 실행하고 기다리는 등 

다양한 작업을 병렬적으로 동시 실행하는 효과를 쉽게 얻을수 있다. 

 

특히 UI 및 서버 애플리케이션에서 매우 중요하다. 

 

 

주의사항

 

async / await 로 사용하는 메서드의 반환 타입은 일반적으로 Task 또는 Task<TResult>다.

예외적으로 이벤트 핸들러(EventHandler)와의 호환성 때문에 void 반환 타입도 사용할 수 있지만,

void를 반환하는 async 메서드는 예외처리가 어렵고 호출자가 해당 메서드의 완료를 추적할 수 없기 때문에

이벤트 핸들러 외의 경우에는 void를 사용하는 것을 지양해야 한다.

 

다시 이야기하자면, 비동기 메서드는 호출자가 실행 상태를 추적할 수 있도록 Task를 반환해야 한다.

이를 통해 메서드 실행 완료 추적, 예외 처리, 병렬 실행 등을 할  수 있기 때문이다.

public async void AsyncVoidMethod() 
{
    await Task.Delay(1000);
    throw new Exception("이 예외는 호출자가 잡을 수 없음");
}

public async Task AsyncTaskMethod() 
{
    await Task.Delay(1000);
    throw new Exception("이 예외는 호출자가 잡을 수 있음");
}

// 예외 처리
try 
{
    await AsyncTaskMethod();  // ✅ 예외가 호출자에게 전달됨
} 
catch (Exception ex) 
{
    Console.WriteLine($"예외 발생: {ex.Message}");
}

 

 

https://www.youtube.com/@i_am_gamer_TV

 

게이머TV

게임을 만들고 싶어하는 사람들을 위한 채널입니다.

www.youtube.com

https://blog.naver.com/i_am_gamer/223504636255

 

유니티 강의 - async await

async와 await 은 C# 5.0 버전부터 지원하는 문법으로 비동기 코드를 쉽게 작성할 수 있게 해줍니다. 스레...

blog.naver.com

 

 

 


 

 

 

질문과 추가 공부

 

1. 비동기 상태 머신은 무엇인가 ?

  • 비동기 코드가 실행되다 중단(await)되고 , 이후 다시 재개될 수 있도록 설계된 구조
  • async/await를 사용하면 컴파일러가 내부적으로 비동기 상태 머신을 생성, 비동기 작업 관리
  • await를 만나면 메서드는 즉시 반환, 실행 상태가 내부 머신에 저장
    => Task가 완료되면 저장되었던 상태에서 다음 코드로 이어서 진행됨

 

2. Task<Result>는 어떻게 동작하고, Task는 어디로 사라지는가 ?

  • 비동키 메서드에서 Task<TResult>를 반환할 때, await를 사용하면 마치 TResult만 반환된 것처럼 보임,
    사실 내부적으로는 Task<TResult>를 관리하는 상태 머신이 동작
public async Task<int> GetNumberAsync()
{
    await Task.Delay(1000);
    return 42;
}

int result = await GetNumberAsync();  // ✅ result에는 42가 저장됨

 

await GetNumberAsync();를 호출하면

  1. Task<int> 반환 ( 아직 값이 없음)
  2. await가 Task<int>를 기다림 , 현재 상태를 저장하고 나중에 이어서 실행
  3. Task.Delay(1000) 실행 , 1초 후 다시 코드 실행
  4. return 42 실행, Task의 Result 값이 42로 설정됨
  5. await 키워드는 Task의 Result 값을 추출하여 int result에 할당

즉, Task 자체가 사라지는 것이 아니라 await이 Task의 결과값을 꺼내서 할당하는 것

 

 

3. Task가 비동기 작업이 진행중인지 완료되었는지를 나타내는 객체라는 말이 무었인가 ?
    더해 await 키워드는 무엇을 의미하고 어떤 역할이 있는가 ? 

 

Task는 비동기 작업의 상태를 추적하는 객체

작업이 진행 중인지, 완료되었는지를 추적할 수 있는 일종의 "약속"과 같은 개념

 

Task의 상태는 다음과 같이 나뉜다.

 

즉, Task는 "상태" 를 담고 있는 "실행이 끝났는지, 아직 진행중인지"를 관리하는 객체.

 

이때 await의 의미와 역할은 ? 

await는 "비동기 작업이 끝날 때까지 기다려 !"라는 의미이다. 

Task가 완료될 때까지 기다리고, Task<TResult>인 경우 반환값을 꺼내고, 메인 스레드를 블로킹하지 않고

다른 작업을 계속 진행하게 한다. 

async Task<int> GetNumberAsync()
{
    await Task.Delay(3000); // 3초 후 실행
    return 42;
}

Task<int> task = GetNumberAsync();
Console.WriteLine(task.Status); // ✅ 아직 실행 중 (Running)

예를 들어 다음과 같은 코드가 있다면 실행 시점에서 task에는 값이 들어갈 수 없다,

아직 await Task.Delay(3000)이 실행중이기 때문에 int 값을 리턴하지 못했기 때문이다.

 

async Task<int> GetNumberAsync()
{
    await Task.Delay(2000); // 2초 대기 (비동기)
    return 42; // ✅ 반환값을 갖는 Task
}

async Task Main()
{
    int result = await GetNumberAsync(); // ✅ 여기서 2초 기다림
    Console.WriteLine(result); // 42 출력
}

 

다음과 같이 await 키워드를 붙여서 실행해주어야 Task의 상태가 완료로 바꿀 때까지 기다렸다가 완료되면 값을 가져오고

다음으로 넘어가 값을 출력할 수 있다.

 

 

이해를 위해 시나리오 하나를 보자면 게임 제작 시, 대기화면에서 데이터를 불러오는 동안 로딩 애니메이션을 돌린다고

가정해보면 다음과 같은 코드를 사용할 수 있다.

using System;
using System.Threading.Tasks;
using UnityEngine;

public class AsyncExample : MonoBehaviour
{
    async void Start()
    {
        Debug.Log("1. 데이터 로딩 시작!");

        // ✅ 데이터 로드를 비동기적으로 실행
        Task<string> loadTask = LoadPlayerDataAsync();

        Debug.Log("2. 로딩 화면 계속 유지 중...");

        // ✅ 데이터 로드가 끝날 때까지 기다렸다가 변수에 저장
        string playerData = await loadTask;

        Debug.Log($"3. 데이터 로드 완료! 플레이어 정보: {playerData}");
    }

    async Task<string> LoadPlayerDataAsync()
    {
        await Task.Delay(3000);  // ✅ 실제로는 서버 요청을 보내는 부분
        return "플레이어 레벨: 25, 경험치: 10000";
    }
}

 

1. "데이터 로딩 시작!" 이 출력된다.

2. LoadPlayerDataAsync();가 실행된다. 

3. await Task.Delay(3000);이 실행되고, await 키워드를 만났기 때문에 제어권이 호출자인 Start()로 돌아게된다.
4. "로딩 화면 계속 유지 중..."이 출력된다.

5. await loadTask를 만나 값이 반환될 때까지 기다렸다가 반환된 값을 playerData에 저장한다

6. "데이터 로드 완료 ! 플레이어 정보 : ~~"가 출력된다.

 

 

await 키워드

 

  • 결과값을 사용해야 하는 경우 : await 키워드는 비동기 작업이 완료될 때까지 기다림
  • 시간 지연과 같이 결과값이 필요하지 않은 경우 : await 키워드는 제어권을 호출자에게 반환

두...시에는 자려고 했는데 어느새 네시 반 ~ 이지만 즐거웠으니 OK 아닐까요 ?