[C#] 다른 결과로 여러 작업 대기

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.


답변

그냥 await그들 모두를 시작한 후 개별적으로 세 가지 작업.

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

var cat = await catTask;
var house = await houseTask;
var car = await carTask;


답변

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의 답변을 참조하십시오.


답변

세 가지 작업을 감안할 때 – 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 개의 신중한 작업이 아닌 임의의 작업 배열이있는 경우에도 유용합니다.asyncawait

그러나 우리는 여전히 그 문제가 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);
}

이 “비동기 폴백을 사용하는 동기화 경로”접근 방식은 특히 동기 완료가 비교적 빈번한 고성능 코드에서 점점 더 일반적입니다. 완료가 항상 비동기 인 경우에는 전혀 도움이되지 않습니다.

여기에 적용되는 추가 사항 :

  1. 최근 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);
    }
  2. 선호 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);
    }
  3. 가능하면, 선호 IsCompletedSuccessfullyStatus == TaskStatus.RanToCompletion; 이것은 현재 .NET Core에 존재 Task하며 어디서나 존재합니다.ValueTask<T>


답변

작업에 저장 한 다음 모두 기다릴 수 있습니다.

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;


답변

모든 오류를 기록하려고하면 코드에 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 가지 가능한 시나리오가 있습니다.

  1. FeedCat에 실패하면 SellHouse가 이미 완료되었습니다. 이 경우에는 괜찮습니다.

  2. SellHouse가 완료되지 않았으며 어느 시점에서 예외로 실패합니다. 예외는 관찰되지 않으며 종료 자 스레드에서 다시 발생합니다.

  3. 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. 두 가지에 대한 설명은 링크를 참조하십시오.

WaitAll 대 WhenAll