3 가지 작업이 있습니다.
private async Task<Cat> FeedCat() {}
private async Task<House> SellHouse() {}
private async Task<Tesla> BuyCar() {}
내 코드를 계속하기 전에 모두 실행해야하며 각 결과도 필요합니다. 어떤 결과도 서로 공통점이 없습니다.
3 가지 작업을 완료 한 다음 결과를 얻으려면 어떻게 전화를 걸어야합니까?
3 가지 작업이 있습니다.
private async Task<Cat> FeedCat() {}
private async Task<House> SellHouse() {}
private async Task<Tesla> BuyCar() {}
내 코드를 계속하기 전에 모두 실행해야하며 각 결과도 필요합니다. 어떤 결과도 서로 공통점이 없습니다.
3 가지 작업을 완료 한 다음 결과를 얻으려면 어떻게 전화를 걸어야합니까?
답변:
를 사용한 후 다음을 사용 WhenAll
하여 결과를 개별적으로 가져올 수 있습니다 await
.
var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();
await Task.WhenAll(catTask, houseTask, carTask);
var cat = await catTask;
var house = await houseTask;
var car = await carTask;
당신은 또한 사용할 수 있습니다 Task.Result
(이 시점까지 그들이 모두 성공적으로 완료되었으므로). 그러나 다른 시나리오에서는 문제가 발생할 수 await
있지만 정확하기 때문에 사용하는 것이 좋습니다 Result
.
WhenAll
이것을 완전히 제거 할 수 있습니다 . 기다림은 작업이 모두 완료 될 때까지 나중에 3 개의 과제를지나 가지 않도록주의합니다.
Task.WhenAll()
작업을 병렬 모드 로 실행할 수 있습니다 . @Servy가 왜 그것을 제거하도록 제안했는지 이해할 수 없습니다. WhenAll
그들 없이는 하나씩 실행됩니다
catTask
에서 반환 될 때까지 이미 실행 중입니다 FeedCat
. 따라서 두 가지 접근 방식이 모두 효과가 있습니다. 유일한 문제는 await
한 번에 하나씩 또는 모두 함께 원하는지 여부 입니다. 오류 처리는 약간 다릅니다.를 사용 하면 오류 중 하나가 일찍 실패하더라도 모두 처리 Task.WhenAll
됩니다 await
.
WhenAll
은 작업 실행시기 또는 실행 방식에 영향을 미치지 않습니다. 그것은 단지 어떤이 가능성 결과를 관찰하는 방법을 초래의를. 이 특별한 경우의 유일한 차이점은 처음 두 가지 방법 중 하나에서 오류가 발생하면 Stephen의 것보다 내 메소드 에서이 호출 스택에서 예외가 발생한다는 것입니다. ).
그냥 await
그들 모두를 시작한 후 개별적으로 세 가지 작업.
var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();
var cat = await catTask;
var house = await houseTask;
var car = await carTask;
Task.WhenAll
변경 사항을 추가하면 문자 그대로 관찰 가능한 방식으로 프로그램의 동작에 대해 아무런 변화가 없습니다. 순전히 중복되는 메소드 호출입니다. 원하는 경우 미적 선택으로 추가 할 수 있지만 코드의 기능은 변경되지 않습니다. 코드의 실행 시간은 해당 메소드 호출의 유무에 관계없이 동일합니다 (기술적 으로 호출하는 데 약간의 오버 헤드 WhenAll
가 있지만 무시할 수 있어야 함) .이 버전 보다이 버전을 실행하는 데 약간 더 오래 걸립니다.
WhenAll
는 순전히 미학적 변화입니다. 동작에서 관찰 할 수있는 유일한 차이점은 이전 작업 오류 (일반적으로 수행 할 필요가없는)가있는 경우 나중에 작업이 완료 될 때까지 대기하는지 여부입니다. 진술이 사실이 아닌 이유에 대한 수많은 설명을 믿지 않는다면 코드를 직접 실행하여 사실이 아님을 알 수 있습니다.
C # 7을 사용하는 경우 다음과 같은 편리한 래퍼 방법을 사용할 수 있습니다.
public static class TaskEx
{
public static async Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
{
return (await task1, await task2);
}
}
... 반환 유형이 다른 여러 작업을 대기하려는 경우 이와 같은 편리한 구문을 사용합니다. 물론 다른 수의 작업을 기다리려면 여러 개의 오버로드를 수행해야합니다.
var (someInt, someString) = await TaskEx.WhenAll(GetIntAsync(), GetStringAsync());
그러나이 예제를 실제로 바꾸려는 경우 ValueTask 및 이미 완료 된 작업에 대한 일부 최적화에 대해서는 Marc Gravell의 답변을 참조하십시오.
Task.WhenAll()
는 튜플을 반환하지 않습니다. 하나는 Result
작업이 반환 한 후 제공된 작업 의 속성 으로 구성 Task.WhenAll()
됩니다.
.Result
다른 사람들이 당신의 모범을 베껴서 나쁜 관행을 영속하지 않도록하려는 Stephen의 추론에 따라 전화를 바꾸는 것이 좋습니다 .
세 가지 작업을 감안할 때 - FeedCat()
, SellHouse()
그리고 BuyCar()
,이 흥미로운 경우가 있습니다 중 그들이 (어떤 이유로, 아마도 캐싱 또는 오류) 모두 완료 기적, 또는 그들이하지 않습니다.
질문에서 우리가 가지고 있다고 가정 해 봅시다.
Task<string> DoTheThings() {
Task<Cat> x = FeedCat();
Task<House> y = SellHouse();
Task<Tesla> z = BuyCar();
// what here?
}
이제 간단한 접근 방식은 다음과 같습니다.
Task.WhenAll(x, y, z);
그러나 ... 결과 처리에 편리하지 않습니다. 우리는 일반적으로 다음을 원합니다 await
.
async Task<string> DoTheThings() {
Task<Cat> x = FeedCat();
Task<House> y = SellHouse();
Task<Tesla> z = BuyCar();
await Task.WhenAll(x, y, z);
// presumably we want to do something with the results...
return DoWhatever(x.Result, y.Result, z.Result);
}
그러나 이것은 많은 오버 헤드를 수행하고 다양한 배열 (배열 포함 params Task[]
)과 목록 (내부)을 할당 합니다. 작동하지만 훌륭한 IMO는 아닙니다. 여러 가지 방법으로 작업 을 사용하는 것이 더 간단 하고 각 async
작업 await
을 차례로 수행 하는 것이 더 간단 합니다 .
async Task<string> DoTheThings() {
Task<Cat> x = FeedCat();
Task<House> y = SellHouse();
Task<Tesla> z = BuyCar();
// do something with the results...
return DoWhatever(await x, await y, await z);
}
위의 주석 중 일부와 달리 await
대신 대신 사용 하면 작업이 실행되는 방식 (동시, 순차적 등)과 아무런 차이Task.WhenAll
가 없습니다 . 최상위 수준에서, / 에 대한 우수한 컴파일러 지원 Task.WhenAll
보다 우선하며, 존재하지 않을 때 유용했습니다 . 3 개의 신중한 작업이 아닌 임의의 작업 배열이있는 경우에도 유용합니다.async
await
그러나 우리는 여전히 그 문제가 async
/ await
지속을위한 컴파일러 많은 소음을 발생합니다. 작업 이 실제로 동 기적으로 완료 될 가능성이있는 경우 비동기 폴백을 사용하여 동기 경로를 빌드하여이를 최적화 할 수 있습니다.
Task<string> DoTheThings() {
Task<Cat> x = FeedCat();
Task<House> y = SellHouse();
Task<Tesla> z = BuyCar();
if(x.Status == TaskStatus.RanToCompletion &&
y.Status == TaskStatus.RanToCompletion &&
z.Status == TaskStatus.RanToCompletion)
return Task.FromResult(
DoWhatever(a.Result, b.Result, c.Result));
// we can safely access .Result, as they are known
// to be ran-to-completion
return Awaited(x, y, z);
}
async Task Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
return DoWhatever(await x, await y, await z);
}
이 "비동기 폴백을 사용하는 동기화 경로"접근 방식은 특히 동기 완료가 비교적 빈번한 고성능 코드에서 점점 더 일반적입니다. 완료가 항상 비동기 인 경우에는 전혀 도움이되지 않습니다.
여기에 적용되는 추가 사항 :
최근 C #에서 async
폴백 방법에 대한 일반적인 패턴 은 일반적으로 로컬 함수로 구현됩니다.
Task<string> DoTheThings() {
async Task<string> Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
return DoWhatever(await a, await b, await c);
}
Task<Cat> x = FeedCat();
Task<House> y = SellHouse();
Task<Tesla> z = BuyCar();
if(x.Status == TaskStatus.RanToCompletion &&
y.Status == TaskStatus.RanToCompletion &&
z.Status == TaskStatus.RanToCompletion)
return Task.FromResult(
DoWhatever(a.Result, b.Result, c.Result));
// we can safely access .Result, as they are known
// to be ran-to-completion
return Awaited(x, y, z);
}
선호 ValueTask<T>
에 Task<T>
많은 다른 반환 값으로 지금까지 완전히 동 기적으로 사물의 좋은 기회가있는 경우 :
ValueTask<string> DoTheThings() {
async ValueTask<string> Awaited(ValueTask<Cat> a, Task<House> b, Task<Tesla> c) {
return DoWhatever(await a, await b, await c);
}
ValueTask<Cat> x = FeedCat();
ValueTask<House> y = SellHouse();
ValueTask<Tesla> z = BuyCar();
if(x.IsCompletedSuccessfully &&
y.IsCompletedSuccessfully &&
z.IsCompletedSuccessfully)
return new ValueTask<string>(
DoWhatever(a.Result, b.Result, c.Result));
// we can safely access .Result, as they are known
// to be ran-to-completion
return Awaited(x, y, z);
}
가능하면, 선호 IsCompletedSuccessfully
에 Status == TaskStatus.RanToCompletion
; 이것은 현재 .NET Core에 존재 Task
하며 어디서나 존재합니다.ValueTask<T>
Task
결과를 사용하지 않고 모두 완료 되면를 반환하는 메소드로 구성했습니다 .
await
예외가 드물지만 의미가 있다는 가정하에 "더 나은"예외 의미론을
작업에 저장 한 다음 모두 기다릴 수 있습니다.
var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();
await Task.WhenAll(catTask, houseTask, carTask);
Cat cat = await catTask;
House house = await houseTask;
Car car = await carTask;
var catTask = FeedCat()
함수를 실행 하지 않고 FeedCat()
결과를 저장하여 일종의 쓸모없는 물건 을 catTask
만듭니다 await Task.WhenAll()
.
모든 오류를 기록하려고하면 코드에 Task.WhenAll 줄을 유지해야합니다. 많은 의견은 코드를 제거하고 개별 작업을 기다릴 수 있다고 제안합니다. Task.WhenAll은 오류 처리에 정말로 중요합니다. 이 줄이 없으면 잠재적으로 관찰되지 않은 예외에 대해 코드를 열어 둡니다.
var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();
await Task.WhenAll(catTask, houseTask, carTask);
var cat = await catTask;
var house = await houseTask;
var car = await carTask;
다음 코드에서 FeedCat에서 예외가 발생한다고 상상해보십시오.
var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();
var cat = await catTask;
var house = await houseTask;
var car = await carTask;
이 경우 houseTask 또는 carTask를 기다리지 않습니다. 여기에는 3 가지 가능한 시나리오가 있습니다.
FeedCat에 실패하면 SellHouse가 이미 완료되었습니다. 이 경우에는 괜찮습니다.
SellHouse가 완료되지 않았으며 어느 시점에서 예외로 실패합니다. 예외는 관찰되지 않으며 종료 자 스레드에서 다시 발생합니다.
SellHouse가 완료되지 않았으며 그 안에 들어 있습니다. 코드가 ASP.NET에서 실행되는 경우 대기 중 일부가 코드 내부에서 완료 되 자마자 SellHouse가 실패합니다. FeedCat이 실패하자마자 기본적으로 화재 및 전화 잊기 및 동기화 컨텍스트가 손실 되었기 때문에 발생합니다.
다음은 사례 (3)에 대해 발생하는 오류입니다.
System.AggregateException: A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread. ---> System.NullReferenceException: Object reference not set to an instance of an object.
at System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
at System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
at System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
at System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
at System.Threading.Tasks.Task.Execute()
--- End of inner exception stack trace ---
---> (Inner Exception #0) System.NullReferenceException: Object reference not set to an instance of an object.
at System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
at System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
at System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
at System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
at System.Threading.Tasks.Task.Execute()<---
사례 (2)의 경우 유사한 오류가 발생하지만 원래 예외 스택 추적이 발생합니다.
.NET 4.0 이상의 경우 TaskScheduler.UnobservedTaskException을 사용하여 관찰되지 않은 예외를 포착 할 수 있습니다. .NET 4.5 이상의 경우 .NET 4.0에 대해 관찰되지 않은 예외가 기본적으로 삼켜집니다. 관찰되지 않은 예외는 프로세스를 중단시킵니다.
자세한 내용 은 .NET 4.5의 작업 예외 처리
스레드 대기 여부에 따라 Task.WhenAll
언급 된대로 또는를 사용할 수 있습니다 Task.WaitAll
. 두 가지에 대한 설명은 링크를 참조하십시오.
Task.WhenAll
결과를 사용 하고 기다립니다.
var tCat = FeedCat();
var tHouse = SellHouse();
var tCar = BuyCar();
await Task.WhenAll(tCat, tHouse, tCar);
Cat cat = await tCat;
House house = await tHouse;
Tesla car = await tCar;
//as they have all definitely finished, you could also use Task.Value.
앞으로 경고
async + await + task tool-set을 사용하여 EntityFramework 를 병렬화 하는 방법을 찾고있는 다른 스레드와 비슷한 스레드를 방문하는 사람들에게 빠른 헤드 업 : 여기에 표시된 패턴은 소리입니다. 그러나 EF의 특수 눈송이에 관해서는 관련된 * Async () 호출마다 별도의 (새) db-context-instance를 사용하지 않는 한 병렬 실행을 달성하십시오.
이러한 종류의 작업은 동일한 ef-db-context 인스턴스에서 여러 쿼리를 병렬로 실행하는 것을 금지하는 ef-db-context의 고유 한 설계 제한으로 인해 필요합니다.
이미 주어진 답변을 활용하여 하나 이상의 작업으로 인해 예외가 발생하는 경우에도 모든 값을 수집 할 수 있습니다.
public async Task<string> Foobar() {
async Task<string> Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
return DoSomething(await a, await b, await c);
}
using (var carTask = BuyCarAsync())
using (var catTask = FeedCatAsync())
using (var houseTask = SellHouseAsync())
{
if (carTask.Status == TaskStatus.RanToCompletion //triple
&& catTask.Status == TaskStatus.RanToCompletion //cache
&& houseTask.Status == TaskStatus.RanToCompletion) { //hits
return Task.FromResult(DoSomething(catTask.Result, carTask.Result, houseTask.Result)); //fast-track
}
cat = await catTask;
car = await carTask;
house = await houseTask;
//or Task.AwaitAll(carTask, catTask, houseTask);
//or await Task.WhenAll(carTask, catTask, houseTask);
//it depends on how you like exception handling better
return Awaited(catTask, carTask, houseTask);
}
}
동일한 성능 특성을 가진 대체 구현은 다음과 같습니다.
public async Task<string> Foobar() {
using (var carTask = BuyCarAsync())
using (var catTask = FeedCatAsync())
using (var houseTask = SellHouseAsync())
{
cat = catTask.Status == TaskStatus.RanToCompletion ? catTask.Result : (await catTask);
car = carTask.Status == TaskStatus.RanToCompletion ? carTask.Result : (await carTask);
house = houseTask.Status == TaskStatus.RanToCompletion ? houseTask.Result : (await houseTask);
return DoSomething(cat, car, house);
}
}
var dn = await Task.WhenAll<dynamic>(FeedCat(),SellHouse(),BuyCar());
Cat에 액세스하려면 다음을 수행하십시오.
var ct = (Cat)dn[0];
이것은 매우 간단하고 사용하기 편리하며 복잡한 솔루션을 따를 필요가 없습니다.
dynamic
악마입니다. 까다로운 COM interop 등을위한 것이며 절대적으로 필요하지 않은 상황에서는 사용해서는 안됩니다. 특히 성능에 관심이 있다면. 또는 타입 안전. 또는 리팩토링. 또는 디버깅.