Favicon

Async modifier, await operator

Peponi11/19/20245m

C#
SyntaxKeywordModifierOperator

1. Introduction

async 한정자는 비동기 프로그래밍을 쉽게 지원하기 위해 C# 5에 도입되었다. 메서드, 무명 메서드, 람다 식 등에 사용할 수 있으며 await 연산자와 함께 사용된다. async 한정자는 로직 내에 await 연산자가 있다는 것을 컴파일러에 알려주며 await 연산자는 지정한 작업이 끝날 때까지 호출자의 스레드가 블락되지 않고 기다릴 수 있게 해준다.

async 한정자는 다음과 같은 리턴 형식을 가질 수 있다.

  • GetAwaiter 메서드가 있는 형식 (Task, ValueTask, ...)
  • void

WARNING

void의 경우 이벤트 처리에만 사용하도록 한다. 일반적으로는 Task를 사용한다.

2. Async 선언

기본적인 async 메서드는 아래와 같다.

리턴이 없는 메서드
async Task Foo()
{
    string a = await Bar();
    Console.WriteLine(a);
}
리턴이 있는 메서드
async Task<string> Foo()
{
    string a = await Bar();
    return a;
}
void
async void Foo()
{
    string a = await Bar();
    Console.WriteLine(a);
}

3. Async, Await

async, await을 메서드에 적용하면 제어는 다음과 같이 흐른다.

private void B_Click(object? sender, EventArgs e)
{
    Foo();
}
 
private async void Foo()
{
    // 호출 thread
    Trace.WriteLine("0 - " + Thread.CurrentThread.ManagedThreadId);
 
    // worker thread
    Task<string> bar = Task.Run(() => Bar());
 
    // 호출 thread. Worker thread가 끝날 때까지 대기
    Trace.WriteLine("1 - " + Thread.CurrentThread.ManagedThreadId);
    await bar;
 
    // 호출 thread
    Trace.WriteLine("2 - " + Thread.CurrentThread.ManagedThreadId);
}
 
private string Bar()
{
    Trace.WriteLine("3 - " + Thread.CurrentThread.ManagedThreadId);
 
    // 시간이 많이 걸리는 작업 가정
    Thread.Sleep(3000);
 
    return "Bar";
}
 
/* output:
0 - 1
1 - 1
3 - 13
2 - 1
*/

위 코드를 보면, 사실상의 기능은 await에 있다. 컴파일러에서는 대기중인 호출 thread가 다른 일을 할 수 있도록 await 연산자가 적용된 지점에 코드 처리를 해준다.

  • await 연산자를 만났을 때 worker thread의 종료를 비동기식 대기한 후 다시 호출자 thread로 제어권이 넘어온다.
  • 이 때, await 이하 구문은 SynchronizationContextPost()를 호출하여 실행하게 된다.

그러나 await 연산자를 사용했다 하여 무조건 worker thread에서 작업을 진행하는 것은 아니다. 아래는 await 연산자를 사용하지만, 호출 thread에서 모든 일을 처리하는 경우이다.

private void B_Click(object? sender, EventArgs e)
{
    Foo();
}
 
private async void Foo()
{
    // 호출 thread
    Trace.WriteLine("0 - " + Thread.CurrentThread.ManagedThreadId);
 
    // 호출 thread
    Task<string> bar = Bar();
 
    // 호출 thread
    Trace.WriteLine("1 - " + Thread.CurrentThread.ManagedThreadId);
    await bar;
 
    // 호출 thread
    Trace.WriteLine("2 - " + Thread.CurrentThread.ManagedThreadId);
}
 
private async Task<string> Bar()
{
    Trace.WriteLine("3 - " + Thread.CurrentThread.ManagedThreadId);
 
    // 시간이 많이 걸리는 작업 가정
    await Task.Delay(3000);
 
    return "Bar";
}
 
/* output:
0 - 1
3 - 1
1 - 1
2 - 1
*/

4. SynchronizationContext가 없는 경우

SynchronizationContext가 없는 경우에는 상황이 다르게 흘러간다. 콘솔 프로그램과 같은 경우 기본적으로 SynchronizationContextnull이기 때문에 await 실행 이후 돌아갈 context가 없게 된다. 따라서 await 이후 작업은 ThreadPool의 thread를 사용하게 된다.

private static async Task Main()
{
    // 호출 thread
    Console.WriteLine("0 - " + Thread.CurrentThread.ManagedThreadId);
 
    // worker thread
    Task<string> bar = Task.Run(() => Bar());
 
    // 호출 thread
    Console.WriteLine("1 - " + Thread.CurrentThread.ManagedThreadId);
    await bar;
 
    // worker thread
    Console.WriteLine("2 - " + Thread.CurrentThread.ManagedThreadId);
}
 
private static string Bar()
{
    Console.WriteLine("3 - " + Thread.CurrentThread.ManagedThreadId);
 
    // 시간이 많이 걸리는 작업 가정
    Thread.Sleep(3000);
 
    return "Bar";
}
 
/* output:
0 - 1
1 - 1
3 - 6
2 - 6
*/

5. 참조 자료