code

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

codestyles 2020. 8. 15. 09:16
반응형

여러 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 모든 작업이 완료 될 때까지 기다린다 는 것 입니다 . 수동으로 순서대로 기다리면 기다리려는 프로그램 부분이 실제로 일찍 계속되기 때문에 예기치 않은 동시성이 발생합니다.

I think it also makes reading the code easier because the semantics that you want are directly documented in code.


My understanding is that the main reason to prefer Task.WhenAll to multiple awaits is performance / task "churning": the DoWork1 method does something like this:

  • start with a given context
  • save the context
  • wait for t1
  • restore the original context
  • save the context
  • wait for t2
  • restore the original context
  • save the context
  • wait for t3
  • restore the original context

By contrast, DoWork2 does this:

  • start with a given context
  • save the context
  • wait for all of t1, t2 and t3
  • restore the original context

Whether this is a big enough deal for your particular case is, of course, "context-dependent" (pardon the pun).


An asynchronous method is implemented as a state-machine. It is possible to write methods so that they are not compiled into state-machines, this is often referred to as a fast-track async method. These can be implemented like so:

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

When using Task.WhenAll it is possible to maintain this fast-track code while still ensuring the caller is able to wait for all tasks to be completed, e.g.:

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

The other answers to this question offer up technical reasons why await Task.WhenAll(t1, t2, t3); is preferred. This answer will aim to look at it from a softer side (which @usr alludes to) while still coming to the same conclusion.

await Task.WhenAll(t1, t2, t3); is a more functional approach, as it declares intent and is atomic.

With await t1; await t2; await t3;, there is nothing preventing a teammate (or maybe even your future self!) from adding code between the individual await statements. Sure, you've compressed it to one line to essentially accomplish that, but that doesn't solve the problem. Besides, it's generally bad form in a team setting to include multiple statements on a given line of code, as it can make the source file harder for human eyes to scan.

Simply put, await Task.WhenAll(t1, t2, t3); is more maintainable, as it communicates your intent more clearly and is less vulnerable to peculiar bugs that can come out of well-meaning updates to the code, or even just merges gone wrong.


(Disclaimer: This answer is taken/inspired from Ian Griffiths' TPL Async course on Pluralsight)

Another reason to prefer WhenAll is Exception handling.

Suppose you had a try-catch block on your DoWork methods, and suppose they were calling different DoTask methods:

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)
    {
        // ...
    }

}

In this case, if all 3 tasks throw exceptions, only the first one will be caught. Any later exception will be lost. I.e. if t2 and t3 throws exception, only t2 will be catched; etc. The subsequent tasks exceptions will go unobserved.

Where as in the WhenAll - if any or all of the tasks fault, the resulting task will contain all of the exceptions. The await keyword still always re-throws the first exception. So the other exceptions are still effectively unobserved. One way to overcome this is to add an empty continuation after the task WhenAll and put the await there. This way if the task fails, the result property will throw the full Aggregate Exception:

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)
    {
        // ...
    }
}

참고URL : https://stackoverflow.com/questions/18310996/why-should-i-prefer-single-await-task-whenall-over-multiple-awaits

반응형