Game Development, 게임개발/개발

Async , Await 키워드, 비동기 프로그래밍(Asynchronous Programming) [Unity]

게임이 더 좋아 2022. 2. 15. 00:36
반응형
728x170

 

기존에도 비동기 프로그래밍 방법이 있었지만

Async와 Await은 C# 5.0부터 더 나은 비동기 프로그래밍을 지원하기 위해 나온 키워드이다.

알아보자

 

우선 Docs의 정의는

  • await 연산자는 피연산자가 나타내는 비동기 작업이 완료될 때까지 바깥쪽 await 메서드의 평가를 일시 중단합니다.
  • async 한정자를 사용하여 메서드, async 또는 무명 메서드를 비동기로 지정합니다. 메서드 또는 식에 이 한정자를 사용하면 비동기 메서드라고 합니다.

 

**참고로 C# 7.0 에서는 이러한 3가지 async 리턴타입에 대한 제약을 넘어 커스텀 리턴 타입을 허용하게 되었다.

-> Custom 리턴 타입의 하나로 .NET Framework에서 ValueTask<T> 라는 타입이 제공되고 있는데, Nuget 패키지에서 System.Threading.Tasks.Extensions 을 설치해서 사용하면 된다.

 


 

Async와 Await은 붙어다니는 사이이다.

그 말뜻이 뭐냐???

 

async는 컴파일러에게 해당 메서드가 await를 가지고 있음을 알려주는 것이다.

다시 말해서 async라고 표시된 메서드는 await를 0개 이상을 가질 수 있다.

** 0개도 컴파일 가능하지만 Warning 메시지가 표시된다.

 

그러니까 async 키워드만으로는 자동으로 비동기 방식으로 프로그램이 수행된다고 생각하면 안된다.

그냥 보조 역할만 해준다고 할 수 있다.

 

일반적으로 async 메서드의 반환형, return type은 대부분의 경우

1. Task<TResult> (리턴값이 있는 경우)

2. Task (리턴값이 없는 경우)

정도가 있다.

 

예를 들어 반환값이 string일 경우

async Task<string> method() 와 같이 정의하고 return "문자열"과 같이 문자열만 리턴한다.

** async를 먼저 쓰고 반환형을 뒤에 쓰고 그 뒤에 메서드가 붙는다.

**반환형이 꺾쇄 안에 들어가있으니 구별이 쉽다.

 

왜 그냥 문자열만 return해도 되느냐???

 

C# 컴파일러는 return 문의 문자열을 자동으로 Task<string>로 변환해 주기 때문이다.

 

사실.. 3번째 반환형인 void 타입이 있는데

특히 이벤트핸들러를 위해 void 리턴을 허용하고 있다.

하지만 메서드 내부에서 Unhandled Exception이 발생하면 프로세스를 다운시키는 문제가 있어서 사용을 자제한 리턴타입이다. -> 쓰긴 씀

**void 반환 비동기 메서드의 호출자는 기다릴 수 없으므로 메서드가 throw하는 예외를 catch할 수 없다.

 


 

보다보면.. async가 보조라고 했으니 await가 중요한 느낌이 팍팍 든다.

그렇다면 어떻게 await를 써야한다는 것일까???

 

await는 일반적으로 Task 혹은 Task<T> 객체와 함께 사용된다.

**Task 클래스에 관한 내용은 이 글을 참조하자.

[Unity, 유니티/Basic, 기본] - Task, Task 클래스 , 비동기 프로그래밍 [Unity]

 

 

하지만 Task 이외의 클래스도 사용 가능한데, 그것은 바로 awaitable 클래스면 된다.

-> 다시 말해서 GetAwaiter() 라는 메서드를 갖는 클래스라면 함께 사용 가능하다.

 



특히 UI 프로그램(★★)에서 await는 Task와 같은 awaitable 클래스의 객체가 완료되기를 기다리는데

여기서 중요한 점은 UI 쓰레드가 정지되지 않고 메시지 루프를 계속 돌 수 있도록 

필요한 코드를 컴파일러가 await 키워드를 만나면 자동으로 추가한다는 점이다.

**메시지 루프가 계속 돌게 만든다는 것이란

-> 마우스 클릭이나 키보드 입력 등과 같은 윈도우 메시지들을 계속 처리할 수 있다는 것을 의미한다.

 

다시 말해서 await는 해당 Task가 끝날 때까지 기다렸다가 완료 후

await 바로 다음 실행문부터 실행을 계속한다.

 

await가 기다리는 Task 혹은 실행 메서드별도의 Worker Thread에서 실행되거나 또는 UI Thread에서 실행된다.

 

예를 하나 보자

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace awaitTest
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void btnRun_Click(object sender, EventArgs e)
        {
            RunIt();
        } 

        // async를 붙인다. -> Await가 있을 것 같네???
        private async void RunIt()
        {
            // 긴 계산을 하는 메서드 비동기로 호출 -> double을 반환하는 Task객체 생성후 바로 람다 식으로 실행
            Task<double> task = Task<double>.Factory.StartNew(() => LongCalc(10));

            // Task가 끝나기를 기다림. 하지만 UI 쓰레드는 Block되지 않는다.
            //즉, 메인 스레드는 task의 결과를 기다리지만 UI 스레드는 다르기에 계속 실행된다.
            await task;

            // Task가 끝난 다음 아래 UI 컨트롤의 값 갱신 -> 메인 스레드로 다시 돌아옴.
            // 이 문장은 UI 쓰레드에서 실행되므로 Invoke()가 필요 없다.
            textBox1.Text = task.Result.ToString();
        }

        double LongCalc(double r)
        {
            // 3초간 연산했다고 가정
            Thread.Sleep(3000);

            return 3.14 * r * r;
        }

    }
}


거의 비슷하지만 다른 예제도 보자

여기서 또 주의 깊게 봐야하는 것은 UI 갱신이다.

 

// 예제
private void button1_Click(object sender, EventArgs e)
{
     Run();  //UI Thread에서 실행 -> button 누르면 실행.
}


//async 키워드가 붙어있는 메서드로 await가 1개 이상 있겠지??
//async 임에도 반환형이 void임 -> UI 이벤트핸들러겠구나??
private async void Run()
{
    // 비동기로 Worker Thread에서 도는 task1
    // Task.Run(): .NET Framework 4.5+
    var task1 = Task.Run(() => LongCalcAsync(10));

    // task1이 끝나길 기다렸다가 끝나면 결과치를 sum에 할당
    // await 키워드가 붙어있는 변수 task1
    int sum = await task1;

    // UI Thread 에서 실행 -> UI 갱신임
    // Control.Invoke 혹은 Control.BeginInvoke 필요없음
    this.label1.Text = "Sum = " + sum;
    this.button1.Enabled = true;
}


//대충 긴 연산..
private int LongCalcAsync(int times)
{
    int result = 0;
    for (int i = 0; i < times; i++)
    {
        result += i;
        Thread.Sleep(1000); 
    }
    return result;
}

 

결과값을 Label 컨트롤에 갱신할 때, Invoke()나 BeginInvoke()를 쓸 필요가 없다는 점이다.

Background Thread에서 비동기 Task가 끝난 후

await가 다시 Caller가 갖고 있던 쓰레드 즉, UI Thread로 다음 문장들을 실행하게 하기 때문이다.

-> 실행되는 async 메서드를 보면 void이고 callback 함수에 의해 실행된다(button click)

 




await가 기다리는 Task는 대부분의 경우 Background Worker Thread에서 실행된다.

하지만 await를 썼다고 해서 자동으로 그 Task(혹은 메서드)가 Worker Thread에서 실행되는 것은 아니다.

 

아래 예제는 await를 사용했지만

해당 Task(LongCalc2)는 (별도로 Worker Thread를 생성하지 않고) UI 쓰레드에서 실행된다.

위에서 말했던 것과 같다.

 

만약 Worker Thread를 생성하려면, Task.Run() 등과 메서드를 사용하여 비동기 작업을 지정할 수 있다.

 

 

private void button1_Click(object sender, EventArgs e)
{
     Run();  //UI Thread에서 실행
}

//async 키워드가 있네?? -> await이 1개이상 존재하겠구나?
private async void Run()
{    
    int sum = await LongCalc2(10); //-> 해당 결과를 반환받을 때까지 대기
    
    //대기 후 갱신
    this.label1.Text = "Sum = " + sum;
    this.button1.Enabled = true;
}

//int를 반환하는 async 메서드네?
private async Task<int> LongCalc2(int times)
{
    //UI Thread에서 실행하는지 확인
    Debug.WriteLine(Thread.CurrentThread.ManagedThreadId);
    
    //시간이 걸리는 연산
    int result = 0;
    for (int i = 0; i < times; i++)
    {
        result += i;                
        await Task.Delay(1000);
    }
    return result;
}
 
 

참고링크

https://www.csharpstudy.com/CSharp/CSharp-async-await.aspx

https://docs.microsoft.com/ko-kr/dotnet/csharp/programming-guide/concepts/async/

https://docs.microsoft.com/ko-kr/dotnet/csharp/language-reference/keywords/async

https://docs.microsoft.com/ko-kr/dotnet/csharp/language-reference/operators/await

 

반응형
그리드형