C# - 비동기 메서드의 async 예약어 유무에 따른 차이
현실적으로 await 예약어를 사용해 호출할 수 있는 대상 메서드는 Task를 반환해야 합니다. 단지, 상황에 따라 async 예약어를 곁들일 수 있는데요, 아래의 예제 코드를 보면,
namespace ConsoleApp1;
internal class Program
{
static async Task Main(string[] args)
{
DateTime now = DateTime.Now;
await DelayTask();
await DelayAsync();
Console.WriteLine($"{(DateTime.Now - now).TotalSeconds}"); // 4초 소요
}
static Task DelayTask()
{
return Task.Delay(1000);
}
static async Task DelayAsync()
{
await Task.Delay(3000);
}
}
결국 await 호출을 하는 측의 입장에서 보면 지정한 시간 동안 대기하는 동작은 똑같지만, DelayTask와 DelayAsync 메서드의 내부 코드에는 살짝(?) 다른 점이 존재합니다. 실제로 위의 예제 코드는 C# 컴파일러에 의해 각각 아래와 같이 처리됩니다.
private static Task DelayTask() // 변화가 없지만,
{
return Task.Delay(2000);
}
[DebuggerStepThrough]
private static Task DelayAsync() // async 예약어로 인해 내부 코드를 상태 머신을 활용하는 코드로 변경
{
Program.<DelayAsync>d__2 <DelayAsync>d__ = new Program.<DelayAsync>d__2();
<DelayAsync>d__.<>t__builder = AsyncTaskMethodBuilder.Create();
<DelayAsync>d__.<>1__state = -1;
<DelayAsync>d__.<>t__builder.Start<Program.<DelayAsync>d__2>(ref <DelayAsync>d__);
return <DelayAsync>d__.<>t__builder.Task;
}
게다가 DelayAsync의 경우 위의 코드뿐만 아니라 내부 코드를 분할해 실행하는 상태 머신을 구현한 클래스를 별도로 하나 더 생성하기 때문에,
[CompilerGenerated]
private sealed class <DelayAsync>d__2 : IAsyncStateMachine
{
// ...[생략]...
void IAsyncStateMachine.MoveNext()
{
// ...[생략]...
}
[DebuggerHidden]
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
}
}
복잡도가 더 증가합니다. 단순히 생각해 봐도, 어차피 같은 역할을 하는 것이라면 async를 적용하지 않는 편이 더 깔끔하고 성능상으로도 (작게나마) 이득을 볼 수 있습니다. 그렇다면 async를 붙이는 적절한 기준이 있을까요?
간단합니다. ^^ 내부 코드에서 await을 써야 한다면 async를 붙이면 되고, 없으면 떼는 것을 권장합니다. 하지만 이런 규칙을 굳이 염두에 두지 않아도 되는 것은, 애당초 Visual Studio 코드 창에서 편집하게 되면 await이 없는 async 메서드에 대해서는 CS1998 경고를 발생하기 때문입니다.
warning CS1998: This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
그래도 저 규칙을 인지하고 있으면, 위와 같은 사례에서 볼 수 있듯이 굳이 DelayAsync와 같이 코딩하는 상황을 DelayTask와 같이 (능동적으로) 바꿔 쓸 수 있을 것입니다. 참고로, 아래의 글에서도,
Long Story Short: Async/Await Best Practices in .NET
; https://medium.com/@deep_blue_day/long-story-short-async-await-best-practices-in-net-1f39d7d84050
"Consider using return Task instead of return await"라는 소제목으로 이에 대한 설명을 하고 있습니다.
그렇긴 한데,
다른 글에서 설명하겠지만 단순히 Task를 반환하는 것은 약간의 혼란이 발생할 수 있는 여지가 있어서 좀 아쉬운 점이 있습니다.
await 호출의 대상이 되는 메서드를 awaitable하다고 하며, 이 조건을 만족하기 위해서는
GetAwaiter 메서드를 가진 타입이기만 하면 됩니다. 일례로 우리가 자주 접하게 되는
Task 타입이 그러한 대표적인 사례입니다. 이후 마이크로소프트가 GC를 괴롭히지 않기 위한 보조 타입으로 만든
ValueTask 역시 GetAwaiter 메서드를 가지고 있습니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]