Introduction
async
한정자는 비동기 프로그래밍을 쉽게 지원하기 위해 C# 5에 도입되었다.
- 메서드, 무명 메서드, 람다 식 등에 사용할 수 있으며,
await
연산자와 함께 사용된다.
async
한정자는 로직 내에 await
연산자가 있다는 것을 컴파일러에 알려준다.
await
연산자는 지정한 작업이 끝날 때까지 호출자의 스레드가 블락되지 않고 기다릴 수 있게 해준다.
async
한정자는 다음과 같은 리턴 형식을 가질 수 있다.
GetAwaiter
메서드가 있는 형식 (Task, Task, ValueTask, ValueTask ...)
- void
void
의 경우 이벤트 처리에만 사용한다. 일반적으로는 Task
를 사용한다.
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);
}
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
*/
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
*/
참조 자료