84

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


  • 병렬로 수행해야하는 작업의 수를 실제로 알지 못하는 경우 어떻게해야합니까? 1000 개의 작업을 실행해야한다면 어떻게해야할까요? 첫 번째는별로 읽을 수 없을 것입니다.await t1; await t2; ....; await tn= > 두 번째 경우 모두 항상 최상의 선택입니다. - cuongle
  • 귀하의 의견을 이해합니다. 나는 최근에 다른 질문과 관련하여 나 자신을 위해 뭔가를 명확하게하려고 애썼다.대답했다. 이 경우 3 가지 작업이있었습니다. - avo

5 답변


80

예, 사용하십시오.WhenAll한 번에 모든 오류를 전파하기 때문입니다. 여러 명이 기다리면 이전에 던진 중 하나가 던지기를 기다리면 오류가 발생합니다.

또 다른 중요한 차이점은 WhenAll이모든완료 할 작업. 사슬의await중단하다기다리는첫 번째 예외는 있지만 대기하지 않는 작업의 실행은 계속됩니다. 이로 인해 예기치 않은 동시성이 발생합니다.

나는 또한 당신이 원하는 의미론이 코드에서 직접 문서화되기 때문에 코드를 더 쉽게 읽게한다고 생각한다.


  • "모든 오류를 즉시 전파하기 때문에"당신이await그 결과. - svick
  • Task를 통해 예외를 관리하는 방법에 대한 질문은이 기사에서 배후에있는 추론에 대한 빠르고 정확한 통찰력을 제공합니다 (여러 번의 대기와 달리 WhenAll의 이점에 대한 지나친 메모도 작성해야합니다).blogs.msdn.com/b/pfxteam/archive/2011/09/28/10217876.aspx - Oskar Lindberg
  • @OskarLindberg OP가 첫 번째 작업을 기다리기 전에 모든 작업을 시작합니다. 그래서 그들은 동시에 실행됩니다. 링크를 가져 주셔서 감사합니다. - usr
  • @Surarning은 동일한 SynchronizationContext를 보존하는 것과 같은 영리한 일을하지 않아도 의미를 제쳐두고 그 이점을 더욱 강화할 수 있는지 궁금했습니다. 결정적인 문서는 찾지 못했지만 IL을 살펴보면 IAsyncStateMachine의 구현이 분명히 다릅니다. IL을 잘 읽지는 못했지만, 적어도 가장 효율적인 IL 코드가 생성되는 것으로 나타났습니다. (어떤 경우 든, WhenAll의 결과가 나와 관련된 모든 작업의 상태를 반영한다는 사실만으로도 대부분의 경우이를 선호할만한 이유입니다.) - Oskar Lindberg
  • 또 다른 중요한 차이점은 WhenAll이 예를 들어 t1 또는 t2가 예외를 던지거나 취소 된 경우에도 모든 작업이 완료 될 때까지 대기한다는 것입니다. - Magnus

20

내 이해가 그 주된 이유는Task.WhenAll복수로await성능 / 작업 "churning"입니다.DoWork1메서드는 다음과 같이 처리합니다.

  • 주어진 것으로 시작하다문맥
  • 문맥을 저장하다
  • t1을 기다려라.
  • 원래의 맥락을 복원한다.
  • 문맥을 저장하다
  • t2를 기다려라.
  • 원래의 맥락을 복원한다.
  • 문맥을 저장하다
  • t3을 기다려라.
  • 원래의 맥락을 복원한다.

대조적으로,DoWork2이것을한다 :

  • 주어진 맥락으로 시작하다
  • 문맥을 저장하다
  • t1, t2 및 t3 모두를 기다린다.
  • 원래의 맥락을 복원한다.

이것은 당신의 특별한 경우에 대해 충분히 큰 거래인지 여부는 물론 "상황에 따라 달라집니다"(장난을 용서하십시오)입니다.


  • 동기화 컨텍스트에 메시지를 보내는 것이 비용이 많이 든다고 생각하는 것 같습니다. 실제로는 그렇지 않습니다. 큐에 추가 된 대리인이 있으며 해당 큐를 읽고 대리자를 실행합니다. 이것이 추가하는 오버 헤드는 솔직히 매우 작습니다. 그것은 아무것도 아니지만 큰 것도 아닙니다. 비동기 작업의 비용은 거의 모든 경우에 이러한 오버 헤드를 줄여줍니다. - Servy
  • 동의했는데, 내가 다른 것보다 하나를 선호한다고 생각할 수있는 유일한 이유였습니다. 글쎄, Task.AaitAll과 유사점은 스레드 스위칭이 더 중요한 비용입니다. - Marcel Popescu
  • @ 안전 마르셀 (Marcel)이 지적한대로, 그것은 정말로 달려 있습니다. 예를 들어 모든 db 작업을 대기 상태로 사용하고 db가 asp.net 인스턴스와 동일한 시스템에있는 경우에는 인라인 된 db 히트를 기다리는 경우가 있습니다. 메모리가 인 - 인덱스, 그 동기화 스위치 및 스레드 풀 셔플보다 저렴합니다. 이런 종류의 시나리오에서 WhenAll ()과 함께 전반적인 승리가있을 수 있습니다. - Chris Moschini
  • @ChrisMoschini DB 쿼리가 서버와 동일한 시스템에있는 DB에 도달하더라도 메시지 펌프에 대리인을 몇 명 추가하는 것보다 더 빠를 것입니다. 이 메모리 내 쿼리는 여전히 확실히 느리게 진행될 것입니다. - Servy
  • 또한 t1이 더 느리고 t2와 t3이 더 빠르면 다른 쪽은 즉시 복귀 할 것을 기다린다. - David Refaeli

14

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

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


2

이 질문에 대한 다른 대답은 기술적 이유를 제시합니다.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);당신의 의도를보다 명확하게 전달하고, 코드에 대한 의미있는 업데이트로 인해 발생할 수있는 특이한 버그에 덜 취약하거나 심지어 잘못 병합되는 경우에도 관리가 용이합니다.


1

(면책 조항 :이 답변은 Ian Griffiths의 TPL Async 과정에서 가져온 / 영감을 받았습니다.Pluralsight)

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 만 캐치됩니다. 후속 작업 예외는 관찰되지 않을 것입니다.

WhereAll의 경우 - 작업의 일부 또는 전부에 오류가있는 경우 결과 작업에 모든 예외가 포함됩니다. await 키워드는 항상 첫 번째 예외를 항상 다시 throw합니다. 그래서 다른 예외는 여전히 효과적으로 관찰되지 않습니다. 이것을 극복하는 한 가지 방법은 WhenAll 작업 후에 빈 연속을 추가하고 거기에 기다리는 것입니다. 이 방법은 작업이 실패하면 결과 속성이 전체 집계 예외를 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)
    {
        // ...
    }
}

연결된 질문


관련된 질문

최근 질문