In attesa di più attività con risultati diversi


237

Ho 3 compiti:

private async Task<Cat> FeedCat() {}
private async Task<House> SellHouse() {}
private async Task<Tesla> BuyCar() {}

Devono essere eseguiti tutti prima che il mio codice possa continuare e ho bisogno anche dei risultati di ciascuno. Nessuno dei risultati ha qualcosa in comune tra loro

Come chiamare e attendere il completamento delle 3 attività e quindi ottenere i risultati?


25
Hai qualche requisito d'ordine? Cioè, non vuoi vendere la casa fino a quando il gatto non viene nutrito?
Eric Lippert,

Risposte:


411

Dopo averlo usato WhenAll, puoi estrarre i risultati individualmente con await:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

Puoi anche usare Task.Result(poiché sai che a questo punto sono stati completati con successo). Tuttavia, ti consiglio di usarlo awaitperché è chiaramente corretto, mentre Resultpuò causare problemi in altri scenari.


83
Puoi semplicemente rimuovere il WhenAllda questo interamente; gli attesi si occuperanno di non passare oltre i 3 incarichi successivi fino a quando le attività non saranno completate.
Servito il

134
Task.WhenAll()consente di eseguire l'attività in modalità parallela . Non riesco a capire perché @Servy abbia suggerito di rimuoverlo. Senza di WhenAllessi saranno gestiti uno ad uno
Sergey G.

87
@Sergey: le attività iniziano a essere eseguite immediatamente. Ad esempio, catTaskè già in esecuzione dal momento in cui è tornato FeedCat. Quindi entrambi gli approcci funzioneranno: l'unica domanda è se li desideri awaituno alla volta o tutti insieme. La gestione degli errori è leggermente diversa: se lo usi Task.WhenAll, awaitli farà tutti, anche se uno di loro fallisce in anticipo.
Stephen Cleary,

23
@Sergey Calling WhenAllnon ha alcun impatto su quando vengono eseguite le operazioni o su come vengono eseguite. Esso solo ha qualche possibilità di effettuare come si osservano i risultati. In questo caso particolare, l'unica differenza è che un errore in uno dei primi due metodi comporterebbe l'eccezione generata in questo stack di chiamate prima nel mio metodo rispetto a quello di Stephen (anche se lo stesso errore verrebbe sempre generato, se ci sono ).
Servito il

37
@Sergey: la chiave è che i metodi asincroni restituiscono sempre attività "calde" (già avviate).
Stephen Cleary,

99

Solo awaitle tre attività separatamente, dopo averle avviate tutte.

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

8
@Bargitta No, è falso. Faranno il loro lavoro in parallelo. Sentiti libero di eseguirlo e vedere di persona.
Servito l'

5
Le persone continuano a porre la stessa domanda dopo anni ... Sento che è importante sottolineare di nuovo che un'attività " inizia a creare " nel corpo della risposta : forse non si preoccupano di leggere i commenti

9
@StephenYork L'aggiunta di Task.WhenAllmodifiche non altera letteralmente il comportamento del programma, in alcun modo osservabile. È una chiamata di metodo puramente ridondante. Se lo desideri, puoi aggiungerlo come scelta estetica, ma non cambia ciò che fa il codice. Il tempo di esecuzione del codice sarà identico con o senza quella chiamata al metodo (beh, tecnicamente ci sarà un overhead molto piccolo per la chiamata WhenAll, ma questo dovrebbe essere trascurabile), rendendo solo quella versione leggermente più lunga da eseguire rispetto a questa versione.
Servito il

4
@StephenYork Il tuo esempio esegue le operazioni in sequenza per due motivi. I tuoi metodi asincroni non sono in realtà asincroni, sono sincroni. Il fatto di disporre di metodi sincroni che restituiscono sempre attività già completate ne impedisce l'esecuzione simultanea. Successivamente, in realtà non fai ciò che viene mostrato in questa risposta di avviare tutti e tre i metodi asincroni e quindi attendere a turno le tre attività. Il tuo esempio non chiama ciascun metodo fino al termine del precedente, impedendo così esplicitamente l'avvio di uno fino al termine del precedente, a differenza di questo codice.
Servito il

4
@MarcvanNieuwenhuijzen Questo è evidentemente non vero, come è stato discusso nei commenti qui e su altre risposte. L'aggiunta WhenAllè un cambiamento puramente estetico. L'unica differenza osservabile nel comportamento è se si attende il completamento delle attività successive in caso di errore di un'attività precedente, che in genere non è necessario. Se non credi alle numerose spiegazioni del perché la tua affermazione non è vera, puoi semplicemente eseguire il codice per te stesso e vedere che non è vero.
Servito il

37

Se stai usando C # 7, puoi usare un pratico metodo wrapper come questo ...

public static class TaskEx
{
    public static async Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
    {
        return (await task1, await task2);
    }
}

... per abilitare una sintassi conveniente come questa quando si desidera attendere più attività con tipi di restituzione diversi. Ovviamente dovresti fare più sovraccarichi per diversi numeri di attività.

var (someInt, someString) = await TaskEx.WhenAll(GetIntAsync(), GetStringAsync());

Tuttavia, vedi la risposta di Marc Gravell per alcune ottimizzazioni intorno a ValueTask e attività già completate se intendi trasformare questo esempio in qualcosa di reale.


Le tuple sono l'unica funzione C # 7 coinvolta qui. Quelli sono sicuramente nella versione finale.
Joel Mueller

Conosco tuple e c # 7. Voglio dire, non riesco a trovare il metodo WhenAll che restituisce tuple. Quale spazio dei nomi / pacchetto?
Yury Scherbakov,

@YuryShcherbakov Task.WhenAll()non sta restituendo una tupla. Uno viene costruito dalle Resultproprietà delle attività fornite dopo il Task.WhenAll()completamento dell'attività restituita .
Chris Charabaruk,

2
Suggerirei di sostituire le .Resultchiamate secondo il ragionamento di Stephen per evitare che altre persone perpetuassero la cattiva pratica copiando il tuo esempio.
julealgon,

Mi chiedo perché questo metodo non faccia parte di questo framework? Sembra così utile Hanno esaurito il tempo e hanno dovuto fermarsi a un solo tipo di ritorno?
Ian Grainger,

14

Dati tre compiti FeedCat(), SellHouse()e BuyCar()ci sono due casi interessanti: o si completano tutti in modo sincrono (per qualche motivo, forse nella cache o in un errore), oppure no.

Diciamo che abbiamo, dalla domanda:

Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();
    // what here?
}

Ora, un approccio semplice sarebbe:

Task.WhenAll(x, y, z);

ma ... non è conveniente per elaborare i risultati; in genere vorremmo awaitche:

async Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    await Task.WhenAll(x, y, z);
    // presumably we want to do something with the results...
    return DoWhatever(x.Result, y.Result, z.Result);
}

ma ciò comporta un sacco di sovraccarico e alloca vari array (incluso l' params Task[]array) ed elenchi (internamente). Funziona, ma non è eccezionale IMO. In molti modi è più semplice usare asyncun'operazione e solo awaita turno:

async Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    // do something with the results...
    return DoWhatever(await x, await y, await z);
}

Contrariamente ad alcuni dei commenti sopra, l'utilizzo awaitinvece di nonTask.WhenAll fa alcuna differenza sul modo in cui le attività vengono eseguite (contemporaneamente, in sequenza, ecc.). Al livello più alto, Task.WhenAll precede un buon supporto del compilatore per async/ awaited era utile quando quelle cose non esistevano . È anche utile quando si dispone di una serie arbitraria di attività, anziché di 3 attività discrete.

Ma: abbiamo ancora il problema che async/ awaitgenera molto rumore del compilatore per la continuazione. Se è probabile che le attività possano effettivamente essere completate in modo sincrono, allora possiamo ottimizzarlo costruendo in un percorso sincrono con un fallback asincrono:

Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    if(x.Status == TaskStatus.RanToCompletion &&
       y.Status == TaskStatus.RanToCompletion &&
       z.Status == TaskStatus.RanToCompletion)
        return Task.FromResult(
          DoWhatever(a.Result, b.Result, c.Result));
       // we can safely access .Result, as they are known
       // to be ran-to-completion

    return Awaited(x, y, z);
}

async Task Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
    return DoWhatever(await x, await y, await z);
}

Questo approccio "sync path with async fallback" è sempre più comune soprattutto nel codice ad alte prestazioni in cui i completamenti sincroni sono relativamente frequenti. Nota che non sarà affatto utile se il completamento è sempre sinceramente asincrono.

Altre cose che si applicano qui:

  1. con il recente C #, un modello comune è che il asyncmetodo di fallback è comunemente implementato come funzione locale:

    Task<string> DoTheThings() {
        async Task<string> Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
            return DoWhatever(await a, await b, await c);
        }
        Task<Cat> x = FeedCat();
        Task<House> y = SellHouse();
        Task<Tesla> z = BuyCar();
    
        if(x.Status == TaskStatus.RanToCompletion &&
           y.Status == TaskStatus.RanToCompletion &&
           z.Status == TaskStatus.RanToCompletion)
            return Task.FromResult(
              DoWhatever(a.Result, b.Result, c.Result));
           // we can safely access .Result, as they are known
           // to be ran-to-completion
    
        return Awaited(x, y, z);
    }
  2. preferire ValueTask<T>a Task<T>se v'è una buona probabilità di cose mai completamente sincrono con molti valori di ritorno differenti:

    ValueTask<string> DoTheThings() {
        async ValueTask<string> Awaited(ValueTask<Cat> a, Task<House> b, Task<Tesla> c) {
            return DoWhatever(await a, await b, await c);
        }
        ValueTask<Cat> x = FeedCat();
        ValueTask<House> y = SellHouse();
        ValueTask<Tesla> z = BuyCar();
    
        if(x.IsCompletedSuccessfully &&
           y.IsCompletedSuccessfully &&
           z.IsCompletedSuccessfully)
            return new ValueTask<string>(
              DoWhatever(a.Result, b.Result, c.Result));
           // we can safely access .Result, as they are known
           // to be ran-to-completion
    
        return Awaited(x, y, z);
    }
  3. se possibile, preferire IsCompletedSuccessfullya Status == TaskStatus.RanToCompletion; questo ora esiste in .NET Core per Taske ovunque perValueTask<T>


"Contrariamente a varie risposte qui, usando wait invece di Task.WhenAll non fa alcuna differenza sul modo in cui le attività vengono eseguite (contemporaneamente, in sequenza, ecc.)" Non vedo alcuna risposta che lo dica. Avrei già commentato loro dicendo tanto se lo facessero. Ci sono molti commenti su molte risposte che lo dicono, ma nessuna risposta. A quale ti riferisci? Si noti inoltre che la risposta non gestisce il risultato delle attività (o si occupa del fatto che i risultati sono tutti di tipo diverso). Li hai composti in un metodo che restituisce solo un Taskquando hanno finito senza usare i risultati.
Serve il

@Servy hai ragione, erano commenti; Aggiungerò un tweak per mostrare usando i risultati
Marc Gravell

Aggiunta modifica @Servy
Marc Gravell

Inoltre, se hai intenzione di eliminare in anticipo le attività sincrone, puoi anche gestire tutte le attività che vengono annullate o guaste in modo sincrono, piuttosto che solo quelle completate correttamente. Se hai deciso che è un'ottimizzazione di cui il tuo programma ha bisogno (che sarà raro, ma accadrà), allora potresti anche andare fino in fondo.
Servito il

@Servy è un argomento complesso - ottieni una semantica delle eccezioni diversa dai due scenari - in attesa di innescare un'eccezione si comporta diversamente dall'accesso a .Result per attivare l'eccezione. A quel punto l'IMO dovremmo awaitottenere la "migliore" semantica delle eccezioni, supponendo che le eccezioni siano rare ma significative
Marc Gravell

12

Puoi archiviarli in attività, quindi attenderli tutti:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

Cat cat = await catTask;
House house = await houseTask;
Car car = await carTask;

non var catTask = FeedCat()esegue la funzione FeedCat()e memorizza il risultato nel catTaskrendere await Task.WhenAll()inutile il tipo di parte poiché il metodo ha già eseguito ??
Kraang Prime,

1
@sanuel se restituiscono l'attività <t>, quindi no ... iniziano l'apertura asincrona, ma non aspettare
Reed Copsey,

Non penso sia accurato, per favore vedi le discussioni sotto la risposta di @ StephenCleary ... vedi anche la risposta di Servy.
Rosdi Kasim,

1
se devo aggiungere .ConfigrtueAwait (false). Lo aggiungerei solo a Task.WhenAll o ad ogni cameriere che segue?
AstroSharp,

@AstroSharp in generale, è una buona idea aggiungerlo a tutti loro (se il primo è completato, viene effettivamente ignorato), ma in questo caso, probabilmente sarebbe giusto fare il primo - a meno che non ci sia più asincrono cose che succederanno dopo.
Reed Copsey,

6

Nel caso in cui si stia tentando di registrare tutti gli errori, assicurarsi di mantenere Task. Quando tutte le righe del codice, molti commenti suggeriscono che è possibile rimuoverlo e attendere singole attività. Task.WhenAll è davvero importante per la gestione degli errori. Senza questa riga potresti potenzialmente lasciare il tuo codice aperto per eccezioni non osservate.

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

Immagina che FeedCat generi un'eccezione nel seguente codice:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

In tal caso, non attenderete mai né houseTask né carTask. Ci sono 3 possibili scenari qui:

  1. SellHouse è già stato completato correttamente quando FeedCat non è riuscito. In questo caso stai bene.

  2. SellHouse non è completo e fallisce con l'eccezione ad un certo punto. L'eccezione non viene osservata e verrà riproposta sul thread del finalizzatore.

  3. SellHouse non è completo e contiene attese al suo interno. Nel caso in cui il tuo codice venga eseguito in ASP.NET SellHouse fallirà non appena alcune delle attese saranno completate al suo interno. Ciò accade perché fondamentalmente hai fatto fuoco e dimentica la chiamata e il contesto di sincronizzazione è andato perso non appena FeedCat ha fallito.

Ecco un errore che otterrai per case (3):

System.AggregateException: A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread. ---> System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
   at System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
   at System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
   at System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
   at System.Threading.Tasks.Task.Execute()
   --- End of inner exception stack trace ---
---> (Inner Exception #0) System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
   at System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
   at System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
   at System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
   at System.Threading.Tasks.Task.Execute()<---

Per case (2) si otterrà un errore simile ma con la traccia dello stack delle eccezioni originale.

Per .NET 4.0 e versioni successive è possibile rilevare eccezioni non osservate utilizzando TaskScheduler.UnobservedTaskException. Per .NET 4.5 e successive le eccezioni non osservate vengono ingerite per impostazione predefinita per l'eccezione non osservata di .NET 4.0 arresta in modo anomalo il processo.

Maggiori dettagli qui: Gestione eccezioni attività in .NET 4.5


2

È possibile utilizzare Task.WhenAllcome indicato o Task.WaitAll, a seconda che si desideri che il thread attenda. Dai un'occhiata al link per una spiegazione di entrambi.

WaitAll vs WhenAll


2

Utilizzare Task.WhenAlle quindi attendere i risultati:

var tCat = FeedCat();
var tHouse = SellHouse();
var tCar = BuyCar();
await Task.WhenAll(tCat, tHouse, tCar);
Cat cat = await tCat;
House house = await tHouse;
Tesla car = await tCar; 
//as they have all definitely finished, you could also use Task.Value.

mm ... non Task.Value (forse esisteva nel 2013?), piuttosto tCat.Result, tHouse.Rultult o tCar.Rultult
Stephen York,

1

Avviso in avanti

Solo un rapido avvertimento per coloro che visitano questo e altri thread simili alla ricerca di un modo per parallelizzare EntityFramework usando il set di strumenti asincrono + wait + task : lo schema mostrato qui è suono, tuttavia, quando si tratta dello speciale fiocco di neve di EF non lo farai ottenere un'esecuzione parallela a meno che e fino a quando non si utilizza un'istanza db-context (nuova) separata all'interno di ogni chiamata * Async () coinvolta.

Questo genere di cose è necessario a causa delle limitazioni progettuali intrinseche dei contesti ef-db che vietano l'esecuzione di più query in parallelo nella stessa istanza di ef-db-context.


Sfruttando le risposte già fornite, questo è il modo per assicurarsi di raccogliere tutti i valori anche nel caso in cui una o più attività si traducano in un'eccezione:

  public async Task<string> Foobar() {
    async Task<string> Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
        return DoSomething(await a, await b, await c);
    }

    using (var carTask = BuyCarAsync())
    using (var catTask = FeedCatAsync())
    using (var houseTask = SellHouseAsync())
    {
        if (carTask.Status == TaskStatus.RanToCompletion //triple
            && catTask.Status == TaskStatus.RanToCompletion //cache
            && houseTask.Status == TaskStatus.RanToCompletion) { //hits
            return Task.FromResult(DoSomething(catTask.Result, carTask.Result, houseTask.Result)); //fast-track
        }

        cat = await catTask;
        car = await carTask;
        house = await houseTask;
        //or Task.AwaitAll(carTask, catTask, houseTask);
        //or await Task.WhenAll(carTask, catTask, houseTask);
        //it depends on how you like exception handling better

        return Awaited(catTask, carTask, houseTask);
   }
 }

Un'implementazione alternativa che ha più o meno le stesse caratteristiche prestazionali potrebbe essere:

 public async Task<string> Foobar() {
    using (var carTask = BuyCarAsync())
    using (var catTask = FeedCatAsync())
    using (var houseTask = SellHouseAsync())
    {
        cat = catTask.Status == TaskStatus.RanToCompletion ? catTask.Result : (await catTask);
        car = carTask.Status == TaskStatus.RanToCompletion ? carTask.Result : (await carTask);
        house = houseTask.Status == TaskStatus.RanToCompletion ? houseTask.Result : (await houseTask);

        return DoSomething(cat, car, house);
     }
 }

-1
var dn = await Task.WhenAll<dynamic>(FeedCat(),SellHouse(),BuyCar());

se vuoi accedere a Cat, fai questo:

var ct = (Cat)dn[0];

Questo è molto semplice da fare e molto utile da usare, non è necessario cercare una soluzione complessa.


1
C'è solo un problema con questo: dynamicè il diavolo. È per interoperabilità COM COM e simili, e non dovrebbe essere usato in nessuna situazione in cui non è assolutamente necessario. Soprattutto se ti preoccupi delle prestazioni. O digitare sicurezza. O refactoring. O debug.
Joel Mueller,
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.