Async modifier, await operator
Peponi │ 11/19/2024 │ 5m
Async modifier, await operator
Peponi
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;
}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이하 구문은SynchronizationContext의Post()를 호출하여 실행하게 된다.
그러나 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가 없는 경우에는 상황이 다르게 흘러간다. 콘솔 프로그램과 같은 경우 기본적으로 SynchronizationContext가 null이기 때문에 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
*/