Task,TaskValue 의 차이


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

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

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

NET에서 비동기 프로그래밍을 사용할 때 'Task’와 'ValueTask’라는 두 가지 유형의 작업을 접할 수 있습니다.
:trophy: 언뜻 보기에는 비슷해 보일 수 있지만 둘 사이에는 성능과 메모리 사용량에 영향을 줄 수 있는 중요한
차이점이 있습니다.
먼저 '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의 비동기 프로그래밍에 유용하지만 성능 및 메모리 특성이 다릅니다.
  • 메모리 사용량:rocket:을 최적화해야 하거나 많은 수의 비동기 작업을 :chart_increasing: 수행하는 경우 'ValueTask’가 더 나은 선택일 수 있습니다.
  • 메모리 사용량이 문제가 되지 않거나 적은 수의 비동기 작업으로 작업하는 경우 'task’으로 충분:bullseye:할 수 있습니다.

추가로

C# - ValueTask와 Task의 성능 비교 https://www.sysnet.pe.kr/2/0/13115?pageno=0#ref_list

C# - Task와 비교해 본 ValueTask 사용법 https://www.sysnet.pe.kr/2/0/13114

4개의 좋아요

혹시 "Task 는 비동기 작업에만 사용됩니다."라고 정리한 이유가 있을까요?

수행하는 작업은 일반적으로 주 애플리케이션 스레드에서 동기적으로 실행되지 않고 스레드 풀 스레드에서 비동기적으로 실행되게 설계되있다고 합니다.

4. Task/Task

Task는 비동기 처리에 사용됩니다. (Task클래스는 내부적으로 Thread로 구현되었습니다.)

https://www.sysnet.pe.kr/3/1/5698
이글도 참조해보세요
좀더 글을 정리하고 싶은데 valuetask, task 관련해서 좀더 연구해보겠습니다.

3개의 좋아요

안녕하세요. 해당 사안에 대해 글마다 견해가 다달라서 질문드립니다.
위의 읽은 글을 토대로 보면,
ValueTask는 스택에 할당, Task는 힙에 할당되는 것으로 보아, 스택이 훨씬 더 빠른 처리 속도를 가진다고 알고있는데, 그렇다면 위에서 말씀하신 Cancel등의 특이 케이스가 아니라면 무조건적인 ValueTask로 사용하는게 좋을까요?

ValueTask 는 아래의 글에 나타난 것 처럼,

.NET Framework: 2039. C# - Task와 비교해 본 ValueTask 사용법 (sysnet.pe.kr)

"이미 결과값을 알고 있는 경우"에 성능 이득을 보여줍니다.

그런데, "값을 알고 있는 경우"는 “특정 입력값에는 언제나 동일한 결과를 반환함” 과 “상당 수의 결과값들을 이미 저장하고 있음” 을 전제합니다.

거시적으로 보면, ValueTask의 사용 여부는 "성능과 자원은 상보적(Trade-off)"이라는 최적화의 원칙을 벗어 나지 않는다고 볼 수 있습니다.

"성능과 자원은 상보적"의 대표적인 예시는 "Recursive call"보다는 Memoization이나 Tabulation이 나은 성능을 제공한다는 점 - 전자는 메모리를 덜 사용하고, 후자는 모든 연산의 결과를 캐싱하기 때문에 메모리(자원)를 더 사용합니다.

닷넷에서도 Task.CompletedTask, string.Empty (요즘은 “” 도 포함된 것 같습니다) 와 같이 사전에 미리 할당 시켜 놓은 것을 재사용하는 힙 할당 회피 기법이 많이 사용됩니다.

그런데, 최적화의 대전제인 "특정 입력값과 출력값 사이에 불변의 상관 관계가 존재함"이 성립하지 않으면, 성능 최적화를 시킬 수단을 찾기는 어려운데, 현실의 비동기 코드가 여기에 해당될 확률이 높습니다.

예를 들어, 특정 id 의 User 정보를 데이터 베이스에서 get 하는 코드가 언제나 같은 데이터로 구성된 User 인스턴스를 반환한다는 보장이 없습니다. 왜냐하면 User의 데이터는 언제 Update 될지 모르니까요. Update 이전에 저장해 놓은 값은 아무런 의미가 없습니다.

이 경우, 호출 코드에서 User 데이터의 상태 변화를 계속 추적해야 하는데, 이는 추적의 내용을 메모리에 저장하고 관리해야 함을 의미합니다. 이렇게 자원을 사전에 소비하고 나서야, 비로소 ValueTask를 통한 성능 이득을 얻을 수 있습니다. 이러한 구조로 되어 있는 대표적인 케이스가 EF 의 DbSet<T>.FindAsync 로, ValueTask<T>를 반환합니다.

닷넷 코어만 쓰는 저의 경험 상, ValueTask로 유의미한 이득을 얻을 수 있는 케이스를 아직까지 체험해보지는 못 했는데, 그 이유는 비동기가 필요한 대부분의 경우, 최적화된 비동기 라이브러리에 의존하거나, 커스텀 코드라도 Task.CompletedTask, Task.FromResult 등 닷넷이 제공하는 성능 최적화 코드를 사용했기 때문일 것입니다.

Cancel 이 "비동기적 실행을 시작하지 않음"을 의미한다면, Cancel 조건은 개발자가 사전에 알고 있기 때문에, Task 를 쓰냐 ValueTask 를 쓰냐는 중요하지 않은, 다시 얘기하면, Task.CompletedTask, Task.FromResult 등이 제공되지 않거나 최적화되지 않았던 시절에만 유의미한 고민이지 않나하는 생가을 해봅니다.

4개의 좋아요