다른 결과로 여러 작업 대기


237

3 가지 작업이 있습니다.

private async Task<Cat> FeedCat() {}
private async Task<House> SellHouse() {}
private async Task<Tesla> BuyCar() {}

내 코드를 계속하기 전에 모두 실행해야하며 각 결과도 필요합니다. 어떤 결과도 서로 공통점이 없습니다.

3 가지 작업을 완료 한 다음 결과를 얻으려면 어떻게 전화를 걸어야합니까?


25
주문 요구 사항이 있습니까? 즉, 고양이에게 먹이를 줄 때까지 집을 팔지 않겠습니까?
Eric Lippert 2016 년

답변:


411

를 사용한 후 다음을 사용 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.


83
WhenAll이것을 완전히 제거 할 수 있습니다 . 기다림은 작업이 모두 완료 될 때까지 나중에 3 개의 과제를지나 가지 않도록주의합니다.
Servy

134
Task.WhenAll()작업을 병렬 모드 로 실행할 수 있습니다 . @Servy가 왜 그것을 제거하도록 제안했는지 이해할 수 없습니다. WhenAll그들 없이는 하나씩 실행됩니다
Sergey G.

87
@Sergey : 작업이 즉시 시작됩니다. 예를 들어 catTask에서 반환 될 때까지 이미 실행 중입니다 FeedCat. 따라서 두 가지 접근 방식이 모두 효과가 있습니다. 유일한 문제는 await한 번에 하나씩 또는 모두 함께 원하는지 여부 입니다. 오류 처리는 약간 다릅니다.를 사용 하면 오류 중 하나가 일찍 실패하더라도 모두 처리 Task.WhenAll됩니다 await.
Stephen Cleary 2016 년

23
@Sergey Calling WhenAll은 작업 실행시기 또는 실행 방식에 영향을 미치지 않습니다. 그것은 단지 어떤이 가능성 결과를 관찰하는 방법을 초래의를. 이 특별한 경우의 유일한 차이점은 처음 두 가지 방법 중 하나에서 오류가 발생하면 Stephen의 것보다 내 메소드 에서이 호출 스택에서 예외가 발생한다는 것입니다. ).
Servy

37
@Sergey : 핵심은 비동기 메소드가 항상 "핫"(이미 시작된) 태스크를 리턴한다는 것입니다.
Stephen Cleary 2016 년

99

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

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

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

8
@Bargitta 아니요, 맞습니다. 그들은 병렬로 작업을 수행합니다. 그것을 실행하고 직접 참조하십시오.
Servy

5
사람들은 년 후 같은 질문을 계속 ... 나는 작업 "고 다시 한번 강조하는 것이 중요하다고 생각 생성에 시작 의 본문에" 대답은 어쩌면 댓글을 읽는 귀찮게하지 않습니다 :

9
@StephenYork Task.WhenAll변경 사항을 추가하면 문자 그대로 관찰 가능한 방식으로 프로그램의 동작에 대해 아무런 변화가 없습니다. 순전히 중복되는 메소드 호출입니다. 원하는 경우 미적 선택으로 추가 할 수 있지만 코드의 기능은 변경되지 않습니다. 코드의 실행 시간은 해당 메소드 호출의 유무에 관계없이 동일합니다 (기술적 으로 호출하는 데 약간의 오버 헤드 WhenAll가 있지만 무시할 수 있어야 함) .이 버전 보다이 버전을 실행하는 데 약간 더 오래 걸립니다.
Servy

4
@StephenYork 예제는 두 가지 이유로 작업을 순차적으로 실행합니다. 비동기 메소드는 실제로 비동기가 아니며 동기식입니다. 항상 이미 완료된 작업을 반환하는 동기 메서드가 있기 때문에 해당 메서드가 동시에 실행되지 않습니다. 다음으로, 세 가지 비동기 메소드를 모두 시작한 다음 세 가지 작업을 차례로 기다리는 이 답변에 실제로 표시된 것을 수행하지 않습니다 . 예제는 이전 코드가 완료 될 때까지 각 메소드를 호출하지 않으므로이 코드와 달리 이전 코드가 완료 될 때까지 메소드가 시작되지 않도록 명시 적으로 금지합니다.
Servy

4
@MarcvanNieuwenhuijzen 그것은 여기의 의견과 다른 답변에서 논의 된 것처럼 사실이 아닙니다. 추가 WhenAll는 순전히 미학적 변화입니다. 동작에서 관찰 할 수있는 유일한 차이점은 이전 작업 오류 (일반적으로 수행 할 필요가없는)가있는 경우 나중에 작업이 완료 될 때까지 대기하는지 여부입니다. 진술이 사실이 아닌 이유에 대한 수많은 설명을 믿지 않는다면 코드를 직접 실행하여 사실이 아님을 알 수 있습니다.
Servy

37

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


튜플은 여기에 관련된 유일한 C # 7 기능입니다. 그것들은 확실히 최종 릴리스에 있습니다.
Joel Mueller

튜플과 C # 7에 대해 알고 있습니다. 튜플을 반환하는 WhenAll 메서드를 찾을 수 없음을 의미합니다. 어떤 네임 스페이스 / 패키지?
Yury Scherbakov

@YuryShcherbakov Task.WhenAll()는 튜플을 반환하지 않습니다. 하나는 Result작업이 반환 한 후 제공된 작업 의 속성 으로 구성 Task.WhenAll()됩니다.
Chris Charabaruk '

2
나는 .Result다른 사람들이 당신의 모범을 베껴서 나쁜 관행을 영속하지 않도록하려는 Stephen의 추론에 따라 전화를 바꾸는 것이 좋습니다 .
julealgon

왜이 방법이 프레임 워크의 일부가 아닌지 궁금합니다. 너무 유용한 것 같습니다. 시간이 없어 단일 반환 유형으로 중지해야합니까?
Ian Grainger

14

세 가지 작업을 감안할 때 - 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>


"여기에서 다양한 대기 응답과는 달리 Task.WhenAll 대신 작업을 실행하는 방법 (현재, 순차적 등)에 차이가 없습니다."라고 말하는 대답이 없습니다. 나는 그들이 한 것처럼 많은 말을 이미했을 것입니다. 많은 답변에 대해 많은 의견이 있지만 답변은 없습니다. 당신은 어느 것을 언급하고 있습니까? 또한 귀하의 답변은 작업 결과를 처리하지 못하거나 결과가 모두 다른 유형이라는 사실을 처리하지 않습니다. Task결과를 사용하지 않고 모두 완료 되면를 반환하는 메소드로 구성했습니다 .
Servy

@Servy 당신 말이 맞아요. 나는 결과를 사용하여 보여주기 위해 비틀기를 추가 할 것이다
Marc Gravell

@Servy tweak 추가
Marc Gravell

또한 동기 작업을 조기에 처리하려는 경우 성공적으로 완료된 작업이 아니라 동 기적으로 취소되거나 오류가 발생한 작업을 처리 할 수도 있습니다. 프로그램이 필요로하는 최적화 (드물지만 일어날 것임)라는 결정을 내렸다면 모든 방향으로 갈 수 있습니다.
Servy

복잡한 주제 인 @Servy-두 시나리오에서 다른 예외 의미를 얻습니다. 예외를 트리거하기를 기다리는 것은 .Result를 액세스하여 예외를 트리거하는 것과 다르게 동작합니다. 그 시점에서 IMO는 await예외가 드물지만 의미가 있다는 가정하에 "더 나은"예외 의미론을
가져와야합니다

12

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

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().
Kraang Prime

1
@sanuel 그들이 <t> 작업을 반환하면 안 돼요 ... 그들은 비동기로 시작하지만 기다리지 않습니다
Reed Copsey

이것이 정확하지 않다고 생각합니다. @StephenCleary의 답변 아래 토론을 참조하십시오. 또한 Servy의 답변을 참조하십시오.
Rosdi Kasim

1
.ConfigrtueAwait (false)를 추가 해야하는 경우. Task.WhenAll 또는 다음에 오는 각 대기자에게 추가합니까?
AstroSharp

@AstroSharp는 일반적으로 모든 것을 추가하는 것이 좋습니다 (첫 번째가 완료되면 효과적으로 무시됩니다). 나중에 일어나는 일들.
리드 콥시

6

모든 오류를 기록하려고하면 코드에 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의 작업 예외 처리


2

스레드 대기 여부에 따라 Task.WhenAll언급 된대로 또는를 사용할 수 있습니다 Task.WaitAll. 두 가지에 대한 설명은 링크를 참조하십시오.

WaitAll 대 WhenAll


2

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.

mm .. Task.Value (2013 년에 존재 했었을 수도 있음)가 아니라 tCat.Result, tHouse.Result 또는 tCar.Result
Stephen York

1

앞으로 경고

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);
     }
 }

-1
var dn = await Task.WhenAll<dynamic>(FeedCat(),SellHouse(),BuyCar());

Cat에 액세스하려면 다음을 수행하십시오.

var ct = (Cat)dn[0];

이것은 매우 간단하고 사용하기 편리하며 복잡한 솔루션을 따를 필요가 없습니다.


1
이것에는 한 가지 문제가 있습니다 : dynamic악마입니다. 까다로운 COM interop 등을위한 것이며 절대적으로 필요하지 않은 상황에서는 사용해서는 안됩니다. 특히 성능에 관심이 있다면. 또는 타입 안전. 또는 리팩토링. 또는 디버깅.
Joel Mueller
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.