ValueTask vs Task

닷넷디벨·2023년 6월 5일
0


linkedin에서 본 흥미로운 Post를 소개할까 합니다.

  • Task에서 ValueTask로 전환해야 합니까?
  • 🔍 결정 방법은 다음과 같습니다.

다음은 해당 포스트에 전문번역본입니다.

NET에서 비동기 프로그래밍을 사용할 때 'Task'와 'ValueTask'라는 두 가지 유형의 작업을 접할 수 있습니다.
🏆 언뜻 보기에는 비슷해 보일 수 있지만 둘 사이에는 성능과 메모리 사용량에 영향을 줄 수 있는 중요한 차이점이 있습니다.
먼저 'Task'이 무엇인지 정의해 보겠습니다. 'Task'는 값을 반환하거나 반환하지 않을 수 있는 비동기 작업을 나타냅니다.
'Task'를 기다리면 호출 스레드가 해제되고 'Task'가 완료될 때까지 다른 코드를 계속 실행할 수 있습니다.
'Task'이 완료되면 결과(있는 경우)가 호출 코드로 반환됩니다.
반면에 'ValueTask'도 비동기 작업이지만 불필요한 할당을 방지하도록 설계되었습니다.
'ValueTask'는 값을 반환하는 작업 또는 아무 것도 반환하지 않는 작업을 나타낼 수 있습니다.
'ValueTask'를 기다리는 경우 동작은 'Task'의 동작과 유사합니다: 호출 스레드가 해제되고 'ValueTask'가 완료될 때까지 다른 코드를 계속 실행할 수 있습니다.
주요 차이점은 메모리 할당과 관련이 있습니다.
'Task'를 기다리면 런타임에서 비동기 작업을 나타내기 위해 'Task' 개체를 할당할 수 있습니다.
이 개체는 일반적으로 힙에 할당되며 많은 'task' 개체를 기다리는 경우 상당한 메모리를 사용할 수 있습니다. 반대로 'ValueTask'는 구조체이므로 힙 대신 스택에 할당할 수 있습니다.
이렇게 하면 성능이 크게 향상되고 많은 수의 비동기 작업을 수행하는 경우 메모리 사용량이 줄어들 수 있습니다.
그렇다면 언제 'Task'대신 'ValueTask'를 사용해야합니까? 대답은 특정 사용 사례에 따라 다릅니다.
적은 수의 비동기 작업으로 작업하거나 메모리 사용량이 문제가 되지 않는 경우 'task'으로 충분할 수 있습니다.
그러나 많은 수의 비동기 작업으로 작업하거나 메모리 사용을 최적화해야 하는 경우 'ValueTask'가 더 나은 선택일 수 있습니다.
특히 cancel 또는 오류 처리를 지원해야 하는 경우 'ValueTask'를 사용하는 것이 'Task'를 사용하는 것보다 더 복잡할 수 있습니다.
이러한 경우 'ValueTask' 빌더를 사용하여 이러한 시나리오를 처리할 수 있는 'ValueTask'를 만들어야 할 수 있습니다.


변역본이라 읽기 좀 불편하실수 있지만 읽어보시길 권합니다.
이 내용을 요약하면

  • ValueTask 는 동기,비동기 작업을 모두 수행 가능합니다.
  • Task 는 비동기 작업에만 사용됩니다.
  • 'Task'와 'ValueTask'는 모두 .NET의 비동기 프로그래밍에 유용하지만 성능 및 메모리 특성이 다릅니다.
  • 메모리 사용량🚀을 최적화해야 하거나 많은 수의 비동기 작업을 📈 수행하는 경우 'ValueTask'가 더 나은 선택일 수 있습니다.
  • 메모리 사용량이 문제가 되지 않거나 적은 수의 비동기 작업으로 작업하는 경우 'task'으로 충분🎯할 수 있습니다.

C# - Task와 비교해 본 ValueTask 사용법

출처: https://www.sysnet.pe.kr/2/0/13114


long result = await CalcAsync(100);
Console.WriteLine(result);

result = await Sum.CalcAsync(100);
Console.WriteLine(result);

return 0;


static Task<long> CalcAsync(int n)
{
    return Task.Run<long>(() =>
    {
        long sum = 0;
        for (int i = 1; i <= n; i++)
        {
            sum += i;
        }
        return sum;
    });
}

public class Sum
{
    static Dictionary<int, long> _cache = new Dictionary<int, long>();

    public static Task<long> CalcAsync(int n)
    {
        if(_cache.ContainsKey(n)==true)
        {
            return Task.FromResult(_cache[n]);
        }
        return Task.Run<long>(() =>
        {
            long sum = 0;
            for(int i= 1; i <= n;i++)
            {
                sum+= i;
            }
            _cache[n] = sum;
            return sum;
        });
    }
}

CalcAsync(int n) 는 일반적인 비동기 작업입니다.
Sum.CalcAsync(100); 같은 작업이나 결과값을 cache에 넣고 task.fromresult로
동기 작업을 합니다.
Cache덕분에 비동기작업을 수행하지않고 바로 동기로 처리하는것죠

여기서 문제는, Task가 바로 참조 타입이라는 점입니다. cache를 구현하기 전의 CalcAsync 메서드라면 비동기 동작을 수행하므로 Task 인스턴스를 반환하는 것이 당연할 수 있지만, 비동기 동작 없이 곧바로 동기 반환을 할 수 있는 경우에도 Task.FromResult를 이용해 굳이 Task를 생성한 다음 반환하는 것은 GC Heap에 필요 없는 할당이 발생하게 되는 것입니다.

바로 이럴 때, 값 형식의 ValueTask를 사용하면 위의 문제를 수정할 수 있습니다.

 public static ValueTask<long> CalcValueAsync(int n)
    {
        if (_cache.ContainsKey(n) == true)
        {
            return new ValueTask<long>(_cache[n]); // 값 형식이므로 여기서 return하면 GC Heap에 아무런 개체도 할당이 되지 않음.
        }

        Task<long> task = Task.Run<long>(() =>
        {
            long sum = 0;
            for (int i = 1; i <= n; i++)
            {
                sum += i;
            }
            _cache[n] = sum;
            return sum;
        });
        return new ValueTask<long>(task); // 기존에는 Task를 반환했지만 반환 타입이 ValueTask로 변경됐으므로 그걸로 래핑해서 반환
    }
  • 결과를 곧바로 반환, 즉 동기적으로 동작할 때는 GC Heap을 사용하지 않으므로 성능이 개선되는 반면
  • 동기 동작을 수행할 때는 기존엔 Task만 반환하면 됐지만, 이제는 ValueTask로 감싸서 반환해야 하는 추가 overhead가 있습니다

즉, 비동기 동작인 경우에도 여전히 Task 개체가 생성된다는 사실에는 변함이 없습니다.

C# - ValueTask와 Task의 성능 비교

출처: https://www.sysnet.pe.kr/2/0/13115?pageno=0#ref_list
자세한 내용은 위에 출처에서 보시길 권합니다.

profile
hardcore developer

0개의 댓글