Quando utilizzare Task.WaitAll rispetto a Task.WhenAll in .NET

La TPL (Task Parallel Library) è una delle novità più interessanti aggiunte nelle recenti versioni di .NET framework. I metodi Task.WaitAll e Task.WhenAll sono due metodi importanti e utilizzati di frequente nel TPL.

Task.WaitAll blocca il thread corrente fino al completamento dell'esecuzione di tutte le altre attività. Il metodo Task.WhenAll viene utilizzato per creare un'attività che verrà completata se e solo se tutte le altre attività saranno state completate.

Quindi, se stai usando Task.WhenAll otterrai un oggetto task che non è completo. Tuttavia, non si bloccherà ma consentirà l'esecuzione del programma. Al contrario, la chiamata al metodo Task.WaitAll in realtà si blocca e attende il completamento di tutte le altre attività.

In sostanza, Task.WhenAll ti darà un'attività che non è completa, ma puoi usare ContinueWith non appena le attività specificate hanno completato la loro esecuzione. Notare che né Task.WhenAll né Task.WaitAll eseguiranno effettivamente le attività; cioè, nessuna attività viene avviata con questi metodi. Ecco come viene utilizzato ContinueWith con Task.WhenAll: 

Task.WhenAll (taskList) .ContinueWith (t => {

  // scrivi qui il tuo codice

});

Come afferma la documentazione di Microsoft, Task.WhenAll "crea un'attività che verrà completata quando tutti gli oggetti Task in una raccolta enumerabile saranno stati completati".

Task.WhenAll vs. Task.WaitAll

Lasciatemi spiegare la differenza tra questi due metodi con un semplice esempio. Supponi di avere un'attività che esegue alcune attività con il thread dell'interfaccia utente, ad esempio, è necessario mostrare alcune animazioni nell'interfaccia utente. Ora, se usi Task.WaitAll, l'interfaccia utente verrà bloccata e non verrà aggiornata finché tutte le attività correlate non saranno state completate e il blocco rilasciato. Tuttavia, se si utilizza Task.WhenAll nella stessa applicazione, il thread dell'interfaccia utente non verrà bloccato e verrà aggiornato come al solito.

Quindi quale di questi metodi dovresti usare e quando? Bene, puoi usare WaitAll quando l'intento si blocca in modo sincrono per ottenere i risultati. Ma quando si desidera sfruttare l'asincronia, è necessario utilizzare la variante WhenAll. Puoi attendere Task.WhenAll senza dover bloccare il thread corrente. Quindi, potresti voler usare await con Task.WhenAll all'interno di un metodo asincrono.

Mentre Task.WaitAll blocca il thread corrente fino al completamento di tutte le attività in sospeso, Task.WhenAll restituisce un oggetto attività. Task.WaitAll genera un'eccezione AggregateException quando una o più attività genera un'eccezione. Quando una o più attività generano un'eccezione e si attende il metodo Task.WhenAll, scarta AggregateException e restituisce solo la prima.

Evita di usare Task.Run in loop

È possibile utilizzare attività quando si desidera eseguire attività simultanee. Se hai bisogno di un alto grado di parallelismo, le attività non sono mai una buona scelta. È sempre consigliabile evitare di utilizzare i thread del pool di thread in ASP.Net. Pertanto, è necessario astenersi dall'utilizzare Task.Run o Task.factory.StartNew in ASP.Net.

Task.Run deve essere sempre utilizzato per il codice associato alla CPU. Task.Run non è una buona scelta nelle applicazioni ASP.Net o nelle applicazioni che sfruttano il runtime ASP.Net poiché trasferisce il lavoro a un thread ThreadPool. Se si utilizza l'API Web ASP.Net, la richiesta utilizzerà già un thread ThreadPool. Pertanto, se si utilizza Task.Run nella propria applicazione API Web ASP.Net, si limita la scalabilità scaricando il lavoro su un altro thread di lavoro senza motivo.

Nota che c'è uno svantaggio nell'usare Task.Run in un ciclo. Se si utilizza il metodo Task.Run all'interno di un ciclo, verranno create più attività, una per ogni unità di lavoro o iterazione. Tuttavia, se si utilizza Parallel.ForEach invece di utilizzare Task.Run all'interno di un ciclo, viene creato un Partitioner per evitare di creare più attività per eseguire l'attività del necessario. Ciò potrebbe migliorare notevolmente le prestazioni in quanto è possibile evitare troppi cambi di contesto e continuare a sfruttare più core nel sistema.

Va notato che Parallel.ForEach utilizza Partitioner internamente in modo da distribuire la raccolta negli elementi di lavoro. Per inciso, questa distribuzione non avviene per ogni attività nell'elenco di elementi, piuttosto avviene come batch. Ciò riduce l'overhead coinvolto e quindi migliora le prestazioni. In altre parole, se usi Task.Run o Task.Factory.StartNew all'interno di un ciclo, creerebbero nuove attività esplicitamente per ogni iterazione nel ciclo. Parallel.ForEach è molto più efficiente perché ottimizzerà l'esecuzione distribuendo il carico di lavoro tra i core multipli del sistema.