여러 awaits보다 단일 'await Task.WhenAll'을 선호해야하는 이유는 무엇입니까?


127

작업 완료 순서에 신경 쓰지 않고 모두 완료해야하는 경우에도 await Task.WhenAll여러 개 대신 사용해야 await합니까? 예를 들어 DoWork2다음과 같은 선호하는 방법 이 있습니다 DoWork1.

using System;
using System.Threading.Tasks;

namespace ConsoleApp
{
    class Program
    {
        static async Task<string> DoTaskAsync(string name, int timeout)
        {
            var start = DateTime.Now;
            Console.WriteLine("Enter {0}, {1}", name, timeout);
            await Task.Delay(timeout);
            Console.WriteLine("Exit {0}, {1}", name, (DateTime.Now - start).TotalMilliseconds);
            return name;
        }

        static async Task DoWork1()
        {
            var t1 = DoTaskAsync("t1.1", 3000);
            var t2 = DoTaskAsync("t1.2", 2000);
            var t3 = DoTaskAsync("t1.3", 1000);

            await t1; await t2; await t3;

            Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
        }

        static async Task DoWork2()
        {
            var t1 = DoTaskAsync("t2.1", 3000);
            var t2 = DoTaskAsync("t2.2", 2000);
            var t3 = DoTaskAsync("t2.3", 1000);

            await Task.WhenAll(t1, t2, t3);

            Console.WriteLine("DoWork2 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
        }


        static void Main(string[] args)
        {
            Task.WhenAll(DoWork1(), DoWork2()).Wait();
        }
    }
}

2
병렬로 수행해야하는 작업의 수를 실제로 모르면 어떻게합니까? 1000 개의 작업을 실행해야한다면 어떨까요? 첫 번째는 가독성 await t1; await t2; ....; await tn이
— 떨어질

귀하의 의견이 의미가 있습니다. 나는 최근 에 대답 한 다른 질문과 관련하여 나 자신을 위해 무언가를 명확히하려고 노력하고 있었다 . 이 경우 3 개의 작업이있었습니다.
— avo '2013-08-19

답변:


113

예, WhenAll모든 오류를 한 번에 전파하므로 사용 하십시오. 다중 대기를 사용하면 이전 대기 중 하나가 발생하면 오류가 손실됩니다.

또 다른 중요한 차이점은 실패 (오류 또는 취소 된 작업)가있는 경우에도WhenAll 모든 작업이 완료 될 때까지 기다린다 는 것 입니다 . 수동으로 순서대로 기다리면 기다리려는 프로그램 부분이 실제로 일찍 계속되기 때문에 예기치 않은 동시성이 발생합니다.

또한 원하는 의미가 코드에 직접 문서화되어 있기 때문에 코드를 더 쉽게 읽을 수 있다고 생각합니다.


9
"모든 오류를 한 번에 전파하기 때문에" await그 결과가 아닙니다 .
— svick

2
예외가 작업과 관리 방법의 문제에 관해서는,이 문서는 (그리고 너무 여러 기다리고 달리 WhenAll의 장점을 통과 메모를 할 일이) 그 뒤에 추론에 대한 빠른하지만 좋은 통찰력을 제공합니다 : 블로그 .msdn.com / b / pfxteam / archive / 2011 / 09 / 28 / 10217876.aspx
— Oskar Lindberg

5
@OskarLindberg OP는 첫 번째 작업을 기다리기 전에 모든 작업을 시작합니다. 그래서 그들은 동시에 실행됩니다. 링크 주셔서 감사합니다.
— usr

3
@usr WhenAll이 동일한 SynchronizationContext를 보존하는 것과 같은 영리한 작업을 수행하지 않는지 여전히 궁금합니다. 결정적인 문서를 찾지 못했지만 IL을 보면 분명히 다른 IAsyncStateMachine 구현이 작동합니다. 나는 IL을 그다지 잘 읽지 못하지만, WhenAll은 적어도 더 효율적인 IL 코드를 생성하는 것처럼 보입니다. (어쨌든, WhenAll의 결과가 나에게 관련된 모든 작업의 ​​상태를 반영한다는 사실만으로도 대부분의 경우 선호하는 이유입니다.)
— Oskar Lindberg 2013

17
또 다른 중요한 차이점은 WhenAll은 예를 들어 t1 또는 t2가 예외를 발생 시키거나 취소되는 경우에도 모든 작업이 완료 될 때까지 기다린다는 것입니다.
— Magnus

28

내 이해는 Task.WhenAll여러 awaits 를 선호하는 주된 이유 는 성능 / 작업 "이탈"입니다.이 DoWork1방법은 다음과 같은 작업을 수행합니다.

  • 주어진 문맥으로 시작
  • 맥락을 저장
  • t1을 기다리다
  • 원래 컨텍스트 복원
  • 맥락을 저장
  • t2를 기다리다
  • 원래 컨텍스트 복원
  • 맥락을 저장
  • t3를 기다리다
  • 원래 컨텍스트 복원

대조적으로 다음을 DoWork2수행합니다.

  • 주어진 문맥으로 시작
  • 맥락을 저장
  • t1, t2 및 t3 모두를 기다립니다.
  • 원래 컨텍스트 복원

이것이 귀하의 특정 사건에 대해 충분히 큰 거래인지 여부는 물론 "문맥 의존적"입니다 (말장난을 용서하십시오).


4
동기화 컨텍스트로 메시지를 보내는 것이 비용이 많이 든다고 생각하는 것 같습니다. 정말 아닙니다. 큐에 추가되는 델리게이트가 있으며 해당 큐를 읽고 델리게이트를 실행합니다. 이것이 추가하는 오버 헤드는 솔직히 매우 적습니다. 아무것도 아니지만 크지도 않습니다. 비동기 작업의 비용은 거의 모든 경우에 이러한 오버 헤드를 축소합니다.
— Servy 2011

동의합니다. 그것이 제가 다른 것보다 하나를 선호한다고 생각할 수있는 유일한 이유였습니다. 글쎄, 그것은 스레드 전환이 더 큰 비용이 드는 Task.WaitAll과의 유사성을 더합니다.
— Marcel Popescu 2013

1
@Servy Marcel은 정말로 의존한다고 지적합니다. 예를 들어 원칙적으로 모든 db 작업에서 await를 사용하고 해당 db가 asp.net 인스턴스와 동일한 시스템에있는 경우 인 메모리 인 인덱스, 더 저렴한 db 히트를 기다리는 경우가 있습니다. 그 동기화 스위치 및 스레드 풀 셔플보다. 그런 종류의 시나리오에서 WhenAll ()을 사용하면 전체적으로 상당한 승리를 거둘 수 있으므로 실제로는 다릅니다.
— Chris Moschini

3
@ChrisMoschini DB 쿼리가 서버와 동일한 시스템에있는 DB에 도달하더라도 메시지 펌프에 몇 명의 대리인을 추가하는 오버 헤드보다 빠를 방법은 없습니다. 그 인 메모리 쿼리는 여전히 거의 확실히 훨씬 느릴 것입니다.
— Servy

또한 t1이 더 느리고 t2와 t3이 더 빠르면 다른 하나는 즉시 반환을 기다립니다.
— David Refaeli

18

비동기 메서드는 상태 머신으로 구현됩니다. 상태 머신으로 컴파일되지 않도록 메서드를 작성할 수 있습니다.이를 종종 빠른 트랙 비동기 메서드라고합니다. 다음과 같이 구현할 수 있습니다.

public Task DoSomethingAsync()
{
    return DoSomethingElseAsync();
}

사용할 때 Task.WhenAll호출자가 모든 작업이 완료 될 때까지 기다릴 수 있도록하면서이 빠른 추적 코드를 유지하는 것이 가능합니다. 예 :

public Task DoSomethingAsync()
{
    var t1 = DoTaskAsync("t2.1", 3000);
    var t2 = DoTaskAsync("t2.2", 2000);
    var t3 = DoTaskAsync("t2.3", 1000);

    return Task.WhenAll(t1, t2, t3);
}

7

(면책 조항 :이 답변은 Pluralsight 에 대한 Ian Griffiths의 TPL Async 과정에서 가져 왔습니다. )

WhenAll을 선호하는 또 다른 이유는 예외 처리입니다.

DoWork 메서드에 try-catch 블록이 있고 다른 DoTask 메서드를 호출한다고 가정합니다.

static async Task DoWork1() // modified with try-catch
{
    try
    {
        var t1 = DoTask1Async("t1.1", 3000);
        var t2 = DoTask2Async("t1.2", 2000);
        var t3 = DoTask3Async("t1.3", 1000);

        await t1; await t2; await t3;

        Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
    }
    catch (Exception x)
    {
        // ...
    }

}

이 경우 3 개 작업 모두에서 예외가 발생하면 첫 번째 작업 만 잡 힙니다. 이후의 모든 예외는 손실됩니다. 즉, t2와 t3이 예외를 던지면 t2 만 잡 힙니다. 등. 후속 작업 예외는 관찰되지 않습니다.

WhenAll에서와 같이-작업의 일부 또는 전부에 오류가있는 경우 결과 작업에 모든 예외가 포함됩니다. await 키워드는 여전히 항상 첫 번째 예외를 다시 발생시킵니다. 따라서 다른 예외는 여전히 효과적으로 관찰되지 않습니다. 이것을 극복하는 한 가지 방법은 WhenAll 작업 뒤에 빈 연속을 추가하고 거기에 await를 두는 것입니다. 이렇게하면 작업이 실패하면 결과 속성이 전체 집계 예외를 throw합니다.

static async Task DoWork2() //modified to catch all exceptions
{
    try
    {
        var t1 = DoTask1Async("t1.1", 3000);
        var t2 = DoTask2Async("t1.2", 2000);
        var t3 = DoTask3Async("t1.3", 1000);

        var t = Task.WhenAll(t1, t2, t3);
        await t.ContinueWith(x => { });

        Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t.Result[0], t.Result[1], t.Result[2]));
    }
    catch (Exception x)
    {
        // ...
    }
}

6

이 질문에 대한 다른 답변은 기술적 인 이유를 제공합니다. await Task.WhenAll(t1, t2, t3); 은 선호 . 이 대답은 @usr이 암시하는 더 부드러운 측면에서 보는 것을 목표로하지만 여전히 동일한 결론에 도달합니다.

await Task.WhenAll(t1, t2, t3); 의도를 선언하고 원자 적이므로보다 기능적인 접근 방식입니다.

를 사용하면 await t1; await t2; await t3;팀원 (또는 미래의 자신)이 개인간에 코드를 추가하는 것을 막을 수 없습니다.await 명령문 . 물론 기본적으로이를 달성하기 위해 한 줄로 압축했지만 문제가 해결되지는 않습니다. 게다가, 팀 설정에서 주어진 코드 줄에 여러 문장을 포함하는 것은 일반적으로 나쁜 형식입니다. 이는 사람의 눈으로 소스 파일을 스캔하기 어렵게 만들 수 있기 때문입니다.

간단히 말해, await Task.WhenAll(t1, t2, t3);의도를 더 명확하게 전달하고 코드에 대한 의미있는 업데이트에서 나올 수있는 특이한 버그에 덜 취약하거나 심지어 병합이 잘못되어 유지되기 때문에 유지 관리가 더 쉽습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.