[C#] C #에서 ValueTask <T>보다 Task <T>를 사용하는 이유는 무엇입니까?

C # 7.0부터 비동기 메소드는 ValueTask <T>를 리턴 할 수 있습니다. 설명은 캐시 된 결과가 있거나 동기 코드를 통해 비동기를 시뮬레이션 할 때 사용해야한다고 말합니다. 그러나 나는 여전히 ValueTask를 사용하는 데 문제가 무엇인지 또는 실제로 async / await가 처음부터 값 유형으로 빌드되지 않은 이유를 이해하지 못합니다. ValueTask가 언제 작업을 수행하지 못합니까?



답변

에서 API를 워드 프로세서 (강조는 추가) :

그것은 그들의 작업의 결과가 동 기적으로 사용할 수 가능성이 있음을 때 방법이 값 타입의 인스턴스를 반환 할 수 있습니다 방법이 너무 자주 새로운 할당의 비용 것을 호출 할 것으로 예상 될 때 Task<TResult>각각의 호출이 금지 될 것입니다.

ValueTask<TResult>대신 을 사용하는 데 따른 단점이 있습니다 Task<TResult>. 예를 들어, ValueTask<TResult>성공적인 결과를 동 기적으로 사용할 수있는 경우 할당을 피하는 데 도움이되지만 Task<TResult>참조 유형은 단일 필드 인 반면 두 개의 필드도 포함 됩니다. 이것은 메소드 호출이 하나의 데이터 대신 두 개의 필드에 해당하는 데이터를 리턴 함을 의미합니다. 또한이 중 하나를 반환하는 메서드가 메서드 내 에서 대기하는 경우 단일 참조 대신 두 개의 필드 인 구조체를 저장해야하기 때문에 async해당 async메서드 의 상태 시스템 이 더 커집니다.

또한, 비아의 비동기 동작의 결과 소모 이외의 용도 await, ValueTask<TResult>실제로 회전 이상 할당으로 이어질 수있는보다 선상 프로그래밍 모델을 초래할 수있다. 예를 들어, Task<TResult>캐시 된 작업이있는 일반 결과로 또는를 반환 할 수있는 방법을 고려 하십시오 ValueTask<TResult>. 결과의 소비자가로 사용하고자하는 경우 Task<TResult>와 같은 방법에서와 같은 사용에 관한, Task.WhenAll그리고 Task.WhenAny의는 ValueTask<TResult>처음으로 변환 할 필요가 Task<TResult>사용 AsTask하는 캐시가있는 경우 피할 것 할당에있는 리드 Task<TResult>사용 된 처음에.

따라서, 비동기 방식의 기본 선택은 반환해야한다 Task또는 Task<TResult>. 성능 분석이 가치있는 것으로 입증 된 경우에만을 ValueTask<TResult>대신 사용해야합니다 Task<TResult>.


답변

그러나 나는 여전히 ValueTask를 사용하는 데 어떤 문제가 있는지 이해하지 못합니다.

구조 유형은 무료가 아닙니다. 참조 크기보다 큰 구조체를 복사하면 참조를 복사하는 것보다 느릴 수 있습니다. 참조보다 큰 구조체를 저장하면 참조를 저장하는 것보다 더 많은 메모리가 필요합니다. 64 비트보다 큰 구조는 참조를 등록 할 수있을 때 등록되지 않을 수 있습니다. 낮은 수집 압력의 이점은 비용을 초과 할 수 없습니다.

엔지니어링 분야에서 성능 문제에 접근해야합니다. 목표를 설정하고 목표 대비 진행 상황을 측정 한 다음 목표가 충족되지 않으면 프로그램을 수정하는 방법을 결정하고 변경 사항이 실제로 개선되는지 확인하십시오.

async / await가 처음부터 값 유형으로 빌드되지 않은 이유는 무엇입니까?

awaitTask<T>유형이 이미 존재 한 후 C #에 추가되었습니다 . 이미 존재하는 새로운 유형을 발명하는 것은 다소 어색했을 것입니다. 그리고 await완벽한이 좋은의 적이다 2012 년 출시 된 하나에 정착하기 전에 수많은 설계 반복을 통해 갔다; 기존 인프라와 잘 작동하는 솔루션을 제공하는 것이 좋으며, 사용자 요구가있는 경우 나중에 개선을 제공하십시오.

또한 사용자 제공 유형을 컴파일러 생성 방법의 출력으로 허용하는 새로운 기능은 상당한 위험과 테스트 부담을 가중시킵니다. 당신이 반환 할 수있는 유일한 것이 무효이거나 작업 인 경우, 테스트 팀은 절대적으로 미친 유형이 반환되는 시나리오를 고려할 필요가 없습니다. 컴파일러를 테스트한다는 것은 사람들이 작성할 있는 프로그램뿐만 아니라 어떤 프로그램을 작성할 수 있는지 파악하는 것을 의미합니다. 컴파일러는 모든 현명한 프로그램뿐만 아니라 모든 합법적 인 프로그램을 컴파일하기를 원하기 때문입니다. 그건 비싸군요.

누군가 ValueTask가 작업을 수행하지 못하는 경우를 설명 할 수 있습니까?

그 목적은 성능 향상입니다. 그렇지 않은 경우는 일을하지 않는 크게 성능을 향상시킬 수 있습니다. 그럴 것이라는 보장은 없습니다.


답변

ValueTask<T>의 하위 집합이 아니며 Task<T>수퍼 세트 입니다.

ValueTask<T>T 자와의 판별 된 조합 인 Task<T>, 그것을 만들기위한 할당없이 ReadAsync<T>동기 (사용하는 반면에 사용할 수있는 T 값을 반환 Task.FromResult<T>할당해야하는 Task<T>인스턴스). ValueTask<T>기다릴 수 있으므로 대부분의 인스턴스 소비는 a와 구분할 수 없습니다 Task<T>.

구조체 인 ValueTask는 API 일관성을 손상시키지 않고 동기식으로 실행될 때 메모리를 할당하지 않는 비동기 메소드를 작성할 수있게합니다. Task 반환 방법과 인터페이스가 있다고 상상해보십시오. 이 인터페이스를 구현하는 각 클래스는 동 기적으로 실행하더라도 (Task.FromResult를 사용하여) Task를 반환해야합니다. 물론 인터페이스에 두 가지 다른 메소드, 동기식 메소드와 비동기식 메소드를 가질 수 있지만 “sync over async”및 “async over sync”를 피하려면 두 가지 다른 구현이 필요합니다.

따라서 비동기식이든 동기식이든 하나의 메소드를 작성하는 것이 아니라 각각에 대해 동일한 메소드를 작성하는 것이 가능합니다. 어디에서나 사용할 수 Task<T>있지만 종종 추가하지 않는 경우가 있습니다.

음, 그것은 한 가지를 추가합니다 : 그것은 메소드가 실제로 ValueTask<T>제공 하는 추가 기능을 사용한다는 암시 적 약속을 호출자에게 추가 합니다. 개인적으로 발신자에게 가능한 한 많은 매개 변수 및 반환 유형을 선택하는 것을 선호합니다. IList<T>열거가 카운트를 제공 할 수 없으면 반환하지 않습니다 . 가능 IEnumerable<T>하다면 반환하지 마십시오 . 소비자는 어떤 메소드를 동 기적으로 호출 할 수 있고 어떤 메소드를 호출 할 수 없는지 알기 위해 문서를 찾을 필요가 없습니다.

나는 미래의 디자인 변화가 확실한 설득력이 없다고 생각합니다. 매우 반대로 : 메소드가 의미를 변경하면 메소드에 대한 모든 호출이 그에 따라 업데이트 될 때까지 빌드를 중단 해야합니다 . 그것이 바람직하지 않다고 생각되면 (그리고 나는 빌드를 중단하지 않으려는 욕망에 동조하고있다) 인터페이스 버전 관리를 고려하십시오.

이것은 본질적으로 강력한 타이핑을위한 것입니다.

상점에서 비동기 메소드를 설계하는 일부 프로그래머가 정보에 근거한 결정을 내릴 수없는 경우, 경험이 부족한 각 프로그래머에게 수석 멘토를 지정하고 매주 코드 검토를하는 것이 도움이 될 수 있습니다. 그들이 틀렸다고 생각하면 왜 다르게해야하는지 설명하십시오. 시니어들에게는 오버 헤드가 있지만, 후배들에게 깊이 빠져들고 따라야 할 임의의 규칙을주는 것보다 훨씬 빠르게 속력을 낼 수 있습니다.

이 방법을 작성한 사람이 동기식으로 호출 할 수 있는지 모르는 경우 지구상 에서 누가 수행합니까?!

비동기 메소드를 작성하는 경험이 부족한 프로그래머가 많은 경우 동일한 사람들이 호출 하는가? 어떤 사람들이 비동기식으로 호출하기에 안전한지 알아낼 자격이 있습니까? 아니면 이러한 것들을 호출하는 방법에 비슷하게 임의의 규칙을 적용하기 시작합니까?

여기서 문제는 반환 유형이 아니라 프로그래머가 준비되지 않은 역할을 맡고 있다는 것입니다. 그 이유가 있었기 때문에 해결하기가 쉽지 않을 것입니다. 그것을 설명하는 것은 확실히 해결책이 아닙니다. 그러나 컴파일러 과거의 문제를 몰래 해결할 방법을 찾는 것도 해결책이 아닙니다.


답변

.Net Core 2.1 에는 몇 가지 변경 사항 이 있습니다 . .net 코어 2.1부터 ValueTask는 동기 완료 조치뿐만 아니라 비동기 완료도 나타낼 수 있습니다. 또한 제네릭이 아닌 ValueTask유형을 받습니다 .

귀하의 질문과 관련된 Stephen Toub 의견 을 남기겠습니다 .

우리는 여전히 지침을 공식화해야하지만 공개 API 표면 영역에서는 다음과 같이 될 것으로 기대합니다.

  • 작업이 가장 유용합니다.

  • ValueTask는 성능 최적화를위한 가장 많은 옵션을 제공합니다.

  • 다른 사람이 무시할 인터페이스 / 가상 메서드를 작성하는 경우 ValueTask가 올바른 기본 선택입니다.

  • 할당이 중요한 핫 경로에서 API를 사용하려면 ValueTask를 사용하는 것이 좋습니다.

  • 그렇지 않으면 성능이 중요하지 않은 경우 더 나은 보증 및 유용성을 제공하므로 기본적으로 작업으로 설정됩니다.

구현 관점에서 반환 된 많은 ValueTask 인스턴스는 여전히 Task에 의해 지원됩니다.

이 기능은 .net 코어 2.1에서만 사용할 수 있습니다. System.Threading.Tasks.Extensions 패키지 와 함께 사용할 수 있습니다 .


답변