안녕하세요. 마수리입니다.
오늘은 많은 사람들이 헷갈려 하는 ValueTask
의 오해에 대해서 이야기 해보는 시간을 가져볼까합니다.
ValueTask
와 Task
가 뭔지는 잘 모르시더라도 ValueTask
는 값
, Task
는 참조
그러면.. 스택
에 할당되는 ValueTask
가 더 오버헤드가 적어서 좋은거 아니야?
그럼, 모든
Task
를 반환하는 객체를ValueTask
를 반환하게 바꾸면 성능이 향상되겠다!
오늘은 이런 질문에 대한 답을 드리도록 하겠습니다.
내부적으로 살펴보면 무조건
ValueTask
를 사용하는 것은 절대 좋은 선택이 아닙니다.
ValueTask
를 반환하는 함수라 하더라도 실제로 await
같은 비동기 호출이 포함된 경우 완료를 기다리는 Task
를 포함한 구조체를 반환하게 됩니다. 그러니까 ValueTask
로 반환한다고 해도 어차피 Task
를 한번 감싼 ValueTask
가 반환되어서 아무런 이득을 취할 수가 없을 뿐더러 밑에서 보시겠지만 더 많은 오버헤드를 일으킵니다.
우선 가장 중심이 되는 ValueTask
가 어떤 데이터를 갖고 있는지 확인해보도록 하겠습니다.
{
private static volatile Task? s_canceledTask;
// 기다릴 필요가 있다면 여기에 Task나 IValueTaskSource가 들어갑니다.
internal readonly object? _obj;
internal readonly short _token;
internal readonly bool _continueOnCapturedContext;
/*생략*/
}
위에보시는 _obj
객체는 ValueTask
를 반환하는 객체가 동기적으로 완료 된다면 null
을 담고 그렇지 않다면 Task
나 IValueTaskSource
가 들어갑니다.
// 이런 함수는 곤란합니다.. 😣
public async ValueTask<int> DoAsync()
{
// 언제나 비동기로 실행되는 함수
return await DoSomethingAsync();
}
위 함수처럼 ValueTask
를 반환하는 함수가 비동기적으로 실행되어 함수 호출이 완료되는 시점을 비동기적으로 기다려야 하는 경우 _obj
객체에 해당 Task
객체를 저장하게 됩니다. 비동기적으로 코드를 기다린다는 것은 내부적으로 GetAwaiter()
를 호출하여 대기 상태에 들어가게 되는데 ValueTask
는 Task
의 TaskAwaiter
가 구현한 ICriticalNotifyCompletion
인터페이스를 ValueTaskAwaiter
를 통해 지원하여 이러한 대기 메커니즘을 제공합니다.
await
가 붙은 코드는 내부적으로 상태머신코드로 변경되며 IsCompleted
나 OnCompleted()
같은 코드들을 이용해서 함수가 완료 되었는지 확인하거나 완료 된 후 진행 될 코드들을 입력하는데 ValueTask
는 이러한 것들을 모두 구현해 두었고 구현부 내부는 모두 Task
객체인 _obj
것들을 호출하는 것으로 구현되어있습니다.
비동기로 종료되는 함수를
ValueTask
로 반환해도 어차피 내부적으론Task
가 사용됩니다.
정말 그럴까요?
ValueTask
의 IsCompleted
와 OnCompleted()
을 직접 보면 확인할 수 있을 것입니다.
public bool IsCompleted
{
get
{
object? obj = _obj;
/* 생략 */
if (obj is Task t)
{
return t.IsCompleted;
}
/* 생략 */
}
}
public void OnCompleted(Action continuation)
{
// _value는 ValueTask이고 obj는 기다릴 Task입니다.
object? obj = _value._obj;
/* 생략 */
if (obj is Task t)
{
t.GetAwaiter().OnCompleted(continuation);
}
/* 생략 */
}
둘 다 모두 Task
의 것을 호출하는 것을 볼 수 있습니다.
결국 처음에 설명 드렸던 것 처럼 ValueTask
를 반환하지만 항상 비동기로 동작하는 함수는 결국 Task
를 포함한 ValueTask
를 반환하고 내부적으론 Task
를 사용하는 형태가 됩니다.
이 말은 그냥
Task
를 반환하는게 낫지 굳이 Warrping 된 구조체를 반환하는 것은 불필요 하다는 것입니다.
이것 외에도 단일 필드인 Task
에 비해 위에서 보신 것 처럼 ValueTask
는 더 많은 필드를 가지고 있어 ValueTask
를 리턴하면 더 많은 데이터 복사가 일어나게 됩니다. 또한, 여러 필드를 가지고 있는 ValueTask
를 대기하기 위해 만들어신 상태 머신 코드는 Task
에 의해 만들어진 상태 코드 머신 보다 커지게 됩니다.
이러한 이유로 ValueTask
가 필요한 곳에만 ValueTask
를 사용하셔야 합니다.
그럼 도대체 언제 ValueTask
를 쓰는게 가장 좋을까요?
그 내용은 다음 주제로 이야기 해보도록 하겠습니다.
감사합니다.