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
*/