Quand utiliser Task.WaitAll par rapport à Task.WhenAll dans .NET

La TPL (Task Parallel Library) est l'une des nouvelles fonctionnalités les plus intéressantes ajoutées dans les versions récentes de .NET Framework. Les méthodes Task.WaitAll et Task.WhenAll sont deux méthodes importantes et fréquemment utilisées dans le TPL.

Le Task.WaitAll bloque le thread en cours jusqu'à ce que toutes les autres tâches aient terminé l'exécution. La méthode Task.WhenAll est utilisée pour créer une tâche qui se terminera si et seulement si toutes les autres tâches sont terminées.

Donc, si vous utilisez Task.WhenAll, vous obtiendrez un objet de tâche qui n'est pas terminé. Cependant, il ne bloquera pas mais permettra au programme de s'exécuter. Au contraire, l'appel de la méthode Task.WaitAll se bloque et attend que toutes les autres tâches se terminent.

Essentiellement, Task.WhenAll vous donnera une tâche qui n'est pas terminée, mais vous pouvez utiliser ContinueWith dès que les tâches spécifiées ont terminé leur exécution. Notez que ni Task.WhenAll ni Task.WaitAll n'exécuteront réellement les tâches; c'est-à-dire qu'aucune tâche n'est lancée par ces méthodes. Voici comment ContinueWith est utilisé avec Task.WhenAll: 

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

  // écrivez votre code ici

});

Comme l'indique la documentation de Microsoft, Task.WhenAll "crée une tâche qui se terminera lorsque tous les objets Task d'une collection énumérable seront terminés."

Task.WhenAll vs Task.WaitAll

Permettez-moi d'expliquer la différence entre ces deux méthodes avec un exemple simple. Supposons que vous ayez une tâche qui effectue une activité avec le thread d'interface utilisateur - par exemple, une animation doit être affichée dans l'interface utilisateur. Désormais, si vous utilisez Task.WaitAll, l'interface utilisateur sera bloquée et ne sera pas mise à jour tant que toutes les tâches associées ne seront pas terminées et le bloc libéré. Cependant, si vous utilisez Task.WhenAll dans la même application, le thread d'interface utilisateur ne sera pas bloqué et sera mis à jour comme d'habitude.

Alors, laquelle de ces méthodes devriez-vous utiliser quand? Eh bien, vous pouvez utiliser WaitAll lorsque l'intention se bloque de manière synchrone pour obtenir les résultats. Mais lorsque vous souhaitez tirer parti de l'asynchronie, vous souhaitez utiliser la variante WhenAll. Vous pouvez attendre Task.WhenAll sans avoir à bloquer le thread en cours. Par conséquent, vous pouvez utiliser await avec Task.WhenAll dans une méthode async.

Alors que Task.WaitAll bloque le thread actuel jusqu'à ce que toutes les tâches en attente soient terminées, Task.WhenAll renvoie un objet de tâche. Task.WaitAll lève une AggregateException lorsqu'une ou plusieurs des tâches lèvent une exception. Lorsqu'une ou plusieurs tâches lèvent une exception et que vous attendez la méthode Task.WhenAll, elle déballe l'exception AggregateException et renvoie uniquement la première.

Évitez d'utiliser Task.Run en boucle

Vous pouvez utiliser des tâches lorsque vous souhaitez exécuter des activités simultanées. Si vous avez besoin d'un degré élevé de parallélisme, les tâches ne sont jamais un bon choix. Il est toujours conseillé d'éviter d'utiliser des threads de pool de threads dans ASP.Net. Par conséquent, vous devez vous abstenir d'utiliser Task.Run ou Task.factory.StartNew dans ASP.Net.

Task.Run doit toujours être utilisé pour le code lié au processeur. Le Task.Run n'est pas un bon choix dans les applications ASP.Net ou les applications qui tirent parti du runtime ASP.Net car il décharge simplement le travail vers un thread ThreadPool. Si vous utilisez l'API Web ASP.Net, la demande utilise déjà un thread ThreadPool. Par conséquent, si vous utilisez Task.Run dans votre application API Web ASP.Net, vous limitez simplement l'évolutivité en déchargeant le travail vers un autre thread de travail sans aucune raison.

Notez qu'il y a un inconvénient à utiliser Task.Run dans une boucle. Si vous utilisez la méthode Task.Run dans une boucle, plusieurs tâches seraient créées - une pour chaque unité de travail ou itération. Cependant, si vous utilisez Parallel.ForEach au lieu d'utiliser Task.Run dans une boucle, un partitionneur est créé pour éviter de créer plus de tâches pour effectuer l'activité que nécessaire. Cela peut améliorer considérablement les performances, car vous pouvez éviter trop de changements de contexte tout en exploitant plusieurs cœurs dans votre système.

Il est à noter que Parallel.ForEach utilise Partitioner en interne afin de distribuer la collection en éléments de travail. Incidemment, cette distribution ne se produit pas pour chaque tâche de la liste des éléments, mais plutôt comme un lot. Cela réduit les frais généraux impliqués et améliore donc les performances. En d'autres termes, si vous utilisez Task.Run ou Task.Factory.StartNew dans une boucle, ils créeraient de nouvelles tâches explicitement pour chaque itération de la boucle. Parallel.ForEach est beaucoup plus efficace car il optimisera l'exécution en répartissant la charge de travail sur les multiples cœurs de votre système.