여러 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.