[C#] 여러 awaits보다 단일 ‘await Task.WhenAll’을 선호해야하는 이유는 무엇입니까?

작업 완료 순서에 신경 쓰지 않고 모두 완료해야하는 경우에도 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();
        }
    }
}



답변

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

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

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


답변

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

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

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

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

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


답변

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

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


답변

(면책 조항 :이 답변은 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)
    {
        // ...
    }
}


답변

이 질문에 대한 다른 답변은 기술적 인 이유를 제공합니다. 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);의도를 더 명확하게 전달하고 코드에 대한 의미있는 업데이트에서 나올 수있는 특이한 버그에 덜 취약하거나 심지어 병합이 잘못되어 유지되기 때문에 유지 관리가 더 쉽습니다.


답변