Gérer les tâches asynchrones et parallèles

Effectué

Pour les développeurs C#, la bibliothèque parallèle de tâches (TPL) offre un moyen plus simple d’écrire du code parallèle. Toutefois, tout le code ne convient pas à la parallélisation. Par exemple, si une boucle effectue uniquement une petite quantité de travail sur chaque itération ou qu’elle ne s’exécute pas pour de nombreuses itérations, la surcharge de parallélisation peut entraîner l’exécution du code plus lentement. En outre, la parallélisation, comme n’importe quel code multithread, ajoute de la complexité à l’exécution de votre programme.

Pièges courants dans les données et le parallélisme des tâches

Dans de nombreux cas, Parallel.For et Parallel.ForEach peut apporter des améliorations significatives des performances sur des boucles séquentielles ordinaires. Toutefois, le travail de parallélisation de la boucle introduit une complexité qui peut entraîner des problèmes qui ne sont pas aussi courants dans le code séquentiel.

Ne partez pas du principe que le parallèle est toujours plus rapide

Dans certains cas, une boucle parallèle peut s’exécuter plus lentement que son équivalent séquentiel. La règle générale est que les boucles parallèles qui ont peu d'itérations et des délégués utilisateur rapides sont peu susceptibles d'entraîner une accélération significative. Toutefois, étant donné que de nombreux facteurs sont impliqués dans les performances, nous vous recommandons de toujours mesurer les résultats réels.

Évitez d’écrire dans des emplacements de mémoire partagée

Dans le code séquentiel, il n’est pas rare de lire ou d’écrire dans des variables statiques ou des champs de classe. Toutefois, chaque fois que plusieurs threads accèdent simultanément à de telles variables, il existe un potentiel significatif pour les conditions de compétition. Même si vous pouvez utiliser des verrous pour synchroniser l’accès à la variable, le coût de la synchronisation peut nuire aux performances. Par conséquent, nous vous recommandons d’éviter, ou au moins de limiter, l’accès à l’état partagé dans une boucle parallèle autant que possible. La meilleure façon de procéder est d'utiliser les surcharges de Parallel.For et Parallel.ForEach qui utilisent une variable System.Threading.ThreadLocal<T> pour stocker l’état local du thread pendant l’exécution de la boucle.

Éviter une surallélisation

En utilisant des boucles parallèles, vous supportez les coûts supplémentaires de répartition de la collection source et de synchronisation des threads de travail. Les avantages de la parallélisation sont encore limités par le nombre de processeurs sur l’ordinateur. Il n’y a pas de vitesse à gagner en exécutant plusieurs threads liés au calcul sur un seul processeur. Il faut donc veiller à ne pas surparalléliser une boucle.

Le scénario le plus courant dans lequel la surallélisation peut se produire se trouve dans des boucles imbriquées. Dans la plupart des cas, il est préférable de paralléliser uniquement la boucle externe, sauf si une ou plusieurs des conditions suivantes s’appliquent :

  • La boucle interne est connue pour être longue.
  • Vous effectuez un calcul coûteux sur chaque commande.
  • Le système cible est connu pour avoir suffisamment de processeurs pour gérer le nombre de threads générés en parallélisant le traitement.

Dans tous les cas, la meilleure façon de déterminer la forme de requête optimale consiste à tester et mesurer.

Gestion des exceptions dans les tâches asynchrones et parallèles

Lorsque vous utilisez la bibliothèque parallèle de tâches (TPL) pour exécuter des tâches, des exceptions peuvent se produire de plusieurs façons différentes. Le plus courant est lorsqu’une tâche déclenche une exception. La levée d'une exception peut se produire lorsque la tâche s'exécute sur un thread du pool de threads ou sur le thread principal. Dans les deux cas, l’exception est propagée au thread appelant.

Lorsque vous utilisez la méthode Task.Wait pour attendre la fin d’une tâche, toutes les exceptions levées par la tâche sont propagées vers le thread appelant. Vous pouvez gérer ces exceptions à l’aide d’un bloc try/catch. Si une tâche est le parent des tâches enfants jointes ou si vous attendez plusieurs tâches, plusieurs exceptions peuvent être levées. Si une ou plusieurs exceptions sont levées, elles sont encapsulées dans une AggregateException instance.

L’exception AggregateException a une InnerExceptions propriété qui peut être énumérée pour examiner toutes les exceptions d’origine levées et gérer (ou non) chacune d’elles individuellement.

L’exemple suivant montre comment gérer les exceptions levées par une tâche.


public static partial class Program
{
    public static void Main()
    {
        HandleThree();
    }
    
    public static void HandleThree()
    {
        var task = Task.Run(
            () => throw new CustomException("This exception is expected!"));

        try
        {
            task.Wait();
        }
        catch (AggregateException ae)
        {
            foreach (var ex in ae.InnerExceptions)
            {
                // Handle the custom exception.
                if (ex is CustomException)
                {
                    Console.WriteLine(ex.Message);
                }
                // Rethrow any other exception.
                else
                {
                    throw ex;
                }
            }
        }
    }
}

// Define the CustomException class
public class CustomException : Exception
{
    public CustomException(string message) : base(message) { }
}
// The example displays the following output:
//        This exception is expected!

Dans cet exemple, la méthode HandleThree crée une tâche qui lève un CustomException. Le bloc try/catch intercepte le AggregateException et parcourt la collection InnerExceptions. Si l’exception est de type CustomException, elle imprime le message dans la console. S’il s’agit d’un autre type d’exception, il la lève à nouveau.

Vous pouvez également gérer les exceptions d’origine à l’aide de la AggregateException.Handle méthode. Cette méthode accepte un délégué appelé pour chaque exception de la collection InnerExceptions. Si le délégué retourne true, l’exception est considérée comme gérée et supprimée de la collection. Si il retourne false, l’exception est levée à nouveau.

L’exemple suivant montre comment utiliser la Handle méthode pour gérer les exceptions levées par une tâche.


public static partial class Program
{
    public static void HandleFour()
    {
        var task = Task.Run(
            () => throw new CustomException("This exception is expected!"));

        try
        {
            task.Wait();
        }
        catch (AggregateException ae)
        {
            ae.Handle(ex =>
            {
                // Handle the custom exception.
                if (ex is CustomException)
                {
                    Console.WriteLine(ex.Message);
                    return true;
                }
                // Rethrow any other exception.
                return false;
            });
        }
    }
}

Dans cet exemple, la méthode HandleFour crée une tâche qui lève un CustomException. Le bloc try/catch intercepte le AggregateException et appelle la méthode Handle. Le délégué vérifie si l’exception est de type CustomException. Si l’exception est de type CustomException, le délégué imprime le message dans la console et retourne true. Une réponse indiquant true que l’exception a été gérée. Si l’exception est un autre type d’exception, le délégué retourne false, ce qui entraîne une nouvelle levée de l’exception.

Résumé

Cette unité décrit les situations où le code n’est pas adapté à la parallélisation et traite des pièges courants dans les données et le parallélisme des tâches. Par exemple, en supposant que le parallèle est toujours plus rapide, l’écriture dans des emplacements de mémoire partagée et la surparallélisation. Le contenu explique également comment gérer des exceptions dans des tâches asynchrones et parallèles, notamment comment utiliser la Task.Wait méthode et la AggregateException.Handle méthode.

Points clés

  • Le code ne convient pas à la parallélisation. Le test et la mesure des performances sont essentiels avant de paralléliser du code.
  • Les pièges courants dans le parallélisme des données et des tâches incluent l’hypothèse que le parallèle est toujours plus rapide, l’écriture dans des emplacements de mémoire partagée et la surallélisation.
  • Les exceptions dans les tâches asynchrones et parallèles peuvent être gérées à l’aide de la Task.Wait méthode et de la AggregateException.Handle méthode.
  • L’exception AggregateException a une InnerExceptions propriété qui peut être énumérée pour examiner toutes les exceptions d’origine levées.