Perché dovrei preferire un singolo "await Task.WhenAll" rispetto a più attende?


127

Nel caso in cui non mi interessa l'ordine di completamento delle attività e ho solo bisogno di completarle tutte, dovrei comunque usarle await Task.WhenAllinvece di più await? ad esempio, è di DoWork2seguito un metodo preferito per DoWork1(e perché?):

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
E se non sapessi effettivamente quante attività devi svolgere in parallelo? Cosa succede se devi eseguire 1000 attività? Il primo non sarà molto leggibile await t1; await t2; ....; await tn=> il secondo è sempre la scelta migliore in entrambi i casi
cuongle

Il tuo commento ha senso. Stavo solo cercando di chiarire qualcosa per me stesso, relativo a un'altra domanda a cui ho risposto di recente . In quel caso, c'erano 3 attività.
evitare il

Risposte:


113

Sì, usa WhenAllperché propaga tutti gli errori contemporaneamente. Con le attese multiple, perdi errori se uno dei precedenti attende lanci.

Un'altra importante differenza è che WhenAllattenderà il completamento di tutte le attività anche in presenza di errori (attività con errore o annullate). Attendere manualmente in sequenza causerebbe una concorrenza imprevista perché la parte del programma che desidera attendere continuerà effettivamente in anticipo.

Penso che renda anche più semplice la lettura del codice perché la semantica che desideri è documentata direttamente nel codice.


9
"Perché propaga tutti gli errori in una volta" No, se il awaitsuo risultato.
svick

2
Per quanto riguarda la questione di come vengono gestite le eccezioni con Task, questo articolo fornisce una rapida ma buona comprensione del ragionamento alla base (e capita di prendere nota anche dei vantaggi di WhenAll in contrasto con le attese multiple): blog .msdn.com / b / pfxteam / archive / 2011/09/28 / 10217876.aspx
Oskar Lindberg

5
@OskarLindberg l'OP sta iniziando tutte le attività prima di attendere la prima. Quindi corrono contemporaneamente. Grazie per il collegamento.
usr

3
@usr Ero ancora curioso di sapere se WhenAll non fa cose intelligenti come conservare lo stesso SynchronizationContext, per spingere ulteriormente i suoi vantaggi oltre alla semantica. Non ho trovato alcuna documentazione conclusiva, ma guardando l'IL ci sono evidentemente diverse implementazioni di IAsyncStateMachine in gioco. Non leggo molto bene IL, ma quanto meno WhenAll sembra generare codice IL più efficiente. (In ogni caso, il solo fatto che il risultato di WhenAll rifletta lo stato di tutte le attività coinvolte per me è una ragione sufficiente per preferirlo nella maggior parte dei casi.)
Oskar Lindberg

17
Un'altra importante differenza è che WhenAll aspetterà il completamento di tutte le attività, anche se, ad esempio, t1 o t2 generano un'eccezione o vengono annullati.
Magnus

28

La mia comprensione è che il motivo principale per preferire Task.WhenAllpiù messaggi awaitè "sfornare" prestazioni / attività: il DoWork1metodo fa qualcosa del genere:

  • inizia con un dato contesto
  • salva il contesto
  • attendere t1
  • ripristinare il contesto originale
  • salva il contesto
  • aspetta t2
  • ripristinare il contesto originale
  • salva il contesto
  • aspetta t3
  • ripristinare il contesto originale

Al contrario, DoWork2fa questo:

  • inizia con un dato contesto
  • salva il contesto
  • attendere tutto t1, t2 e t3
  • ripristinare il contesto originale

Se questo è un affare abbastanza grande per il tuo caso particolare è, ovviamente, "dipendente dal contesto" (scusate il gioco di parole).


4
Sembra che tu pensi che inviare un messaggio al contesto di sincronizzazione sia costoso. Non lo è davvero. Hai un delegato che viene aggiunto a una coda, quella coda verrà letta e il delegato eseguito. L'overhead che questo aggiunge è onestamente molto piccolo. Non è niente, ma non è nemmeno grande. La spesa di qualunque siano le operazioni asincrone farà impallidire tale overhead in quasi tutti i casi.
Servizio

D'accordo, era solo l'unica ragione per cui potevo pensare di preferire l'una all'altra. Bene, questo più la somiglianza con Task.WaitAll dove il cambio di thread è un costo più significativo.
Marcel Popescu

1
@Servy Come sottolinea Marcel che DAVVERO dipende. Ad esempio, se usi await su tutte le attività db per principio e quel db si trova sulla stessa macchina dell'istanza asp.net, ci sono casi in cui attendi un hit db che è in-memory in-index, più economico di quello interruttore di sincronizzazione e threadpool shuffle. Potrebbe esserci una vittoria complessiva significativa con WhenAll () in quel tipo di scenario, quindi ... dipende davvero.
Chris Moschini

3
@ChrisMoschini Non c'è modo che la query DB, anche se colpisce un DB che si trova sulla stessa macchina del server, sarà più veloce dell'overhead di aggiungere alcuni delegati a un message pump. Quella query in memoria sarà quasi certamente ancora molto più lenta.
Servy

Si noti inoltre che se t1 è più lento e t2 e t3 sono più veloci, l'altro attende il ritorno immediatamente.
David Refaeli

18

Un metodo asincrono è implementato come macchina a stati. È possibile scrivere metodi in modo che non vengano compilati in macchine a stati, questo viene spesso definito un metodo asincrono rapido. Questi possono essere implementati in questo modo:

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

Quando si utilizza Task.WhenAllè possibile mantenere questo codice di accesso rapido assicurandosi comunque che il chiamante sia in grado di attendere il completamento di tutte le attività, ad esempio:

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

(Disclaimer: questa risposta è presa / ispirata dal corso TPL Async di Ian Griffiths su Pluralsight )

Un altro motivo per preferire WhenAll è la gestione delle eccezioni.

Supponi di avere un blocco try-catch sui tuoi metodi DoWork e supponiamo che stiano chiamando diversi metodi 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)
    {
        // ...
    }

}

In questo caso, se tutte e 3 le attività generano eccezioni, verrà rilevata solo la prima. Qualsiasi eccezione successiva verrà persa. Ad esempio, se t2 e t3 generano un'eccezione, verrà intercettato solo t2; ecc. Le eccezioni relative alle attività successive passeranno inosservate.

Dove come in WhenAll - se una o tutte le attività hanno un errore, l'attività risultante conterrà tutte le eccezioni. La parola chiave await ripropone sempre la prima eccezione. Quindi le altre eccezioni sono ancora effettivamente inosservate. Un modo per ovviare a questo problema è aggiungere una continuazione vuota dopo l'attività WhenAll e mettere l'attesa lì. In questo modo, se l'attività fallisce, la proprietà del risultato genererà l'eccezione aggregata completa:

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

Le altre risposte a questa domanda offrono ragioni tecniche per cui await Task.WhenAll(t1, t2, t3); è preferibile. Questa risposta mirerà a guardarla da un lato più morbido (a cui allude @usr) pur arrivando alla stessa conclusione.

await Task.WhenAll(t1, t2, t3); è un approccio più funzionale, poiché dichiara l'intento ed è atomico.

Con await t1; await t2; await t3;, non c'è nulla che impedisce a un compagno di squadra (o forse anche al tuo futuro sé!) Di aggiungere codice tra l'individuoawait dichiarazioni. Certo, l'hai compresso su una riga per farlo essenzialmente, ma questo non risolve il problema. Inoltre, è generalmente una cattiva forma in un ambiente di squadra includere più istruzioni su una data riga di codice, poiché può rendere più difficile la scansione del file sorgente per gli occhi umani.

In poche parole, await Task.WhenAll(t1, t2, t3);è più manutenibile, poiché comunica il tuo intento in modo più chiaro ed è meno vulnerabile a bug particolari che possono derivare da aggiornamenti ben intenzionati del codice o anche solo da unioni andate male.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.