Introduction à PLINQ

Parallel LINQ (PLINQ) est une implémentation parallèle du modèle de Language Integrated Query (LINQ). PLINQ implémente le jeu complet d’opérateurs de requête standard LINQ comme méthodes d’extension pour l’espace de noms System.Linq et inclut des opérateurs supplémentaires pour les opérations parallèles. PLINQ combine la simplicité et la lisibilité de la syntaxe LINQ à la puissance de la programmation parallèle.

Conseil

Si vous n’êtes pas familiarisé avec LINQ, il dispose d’un modèle unifié pour interroger toute source de données énumérable d’une manière de type sécurisé. LINQ to Objects est le nom des requêtes LINQ exécutées sur les collections en mémoire telles que List<T> et les tableaux. Cet article suppose que vous avez une connaissance de base de LINQ. Pour plus d’informations, consultez LINQ (Language-Integrated Query).

Qu’est une requête parallèle ?

Une requête PLINQ ressemble à bien des égards à une requête LINQ to Objects non parallèle. Les requêtes PLINQ, tout comme les requêtes LINQ séquentielles, fonctionnent sur toutes les sources de données IEnumerable ou IEnumerable<T> en mémoire et ont une exécution différée, ce qui signifie que leur exécution ne démarre pas tant que la requête est énumérée. La principale différence est que PLINQ essaie d’utiliser pleinement tous les processeurs sur le système. Cela s’effectue par le partitionnement de la source de données en segments et l’exécution de la requête sur chaque segment sur des threads de travail distincts en parallèle sur plusieurs processeurs. Dans de nombreux cas, l’exécution parallèle signifie une exécution beaucoup plus rapide de la requête.

L’exécution parallèle permet à PLINQ d’améliorer de manière significative les performances du code hérité pour certains types de requêtes, souvent par le simple ajout de l’opération de requête AsParallel à la source de données. Toutefois, le parallélisme peut présenter ses propres complexités et toutes les opérations de requête ne s’exécutent pas plus rapidement dans PLINQ. En fait, la parallélisation ralentit réellement certaines requêtes. Par conséquent, vous devez comprendre comment des problèmes, tels que ceux liés à l’ordre, affectent les requêtes parallèles. Pour plus d’informations, consultez Fonctionnement de l’accélération dans PLINQ.

Notes

Cette documentation utilise des expressions lambda pour définir les délégués en PLINQ. Si les expressions lambda en C# ou Visual Basic ne vous sont pas familières, consultez la page Expressions lambda en PLINQ et dans la bibliothèque parallèle de tâches.

Le reste de cet article donne une vue d’ensemble des principales classes PLINQ et explique comment créer des requêtes PLINQ. Chaque section contient des liens vers des exemples de code et des informations plus détaillées.

Classe ParallelEnumerable

La classe System.Linq.ParallelEnumerable expose presque toutes les fonctionnalités de PLINQ. Celle-ci et le reste des types d’espaces de noms System.Linq sont compilés dans l’assembly System.Core.dll. Les projets C# et Visual Basic par défaut de Visual Studio font tous deux référence à l’assembly et importent l’espace de noms.

ParallelEnumerable inclut les implémentations de tous les opérateurs de requête standard pris en charge par LINQ to Objects, bien qu’il ne tente pas de paralléliser chacun d’eux. Si vous n’êtes pas familiarisé avec LINQ, consultez Présentation de LINQ (C#) et Présentation de LINQ (Visual Basic).

Outre les opérateurs de requête standard, la classe ParallelEnumerable contient un ensemble de méthodes qui activent des comportements spécifiques à l’exécution parallèle. Ces méthodes spécifiques de PLINQ sont répertoriées dans le tableau suivant.

Opérateur ParallelEnumerable Description
AsParallel Point d’entrée de PLINQ. Indique que le reste de la requête doit être parallélisé, si possible.
AsSequential Indique que le reste de la requête doit être exécuté de manière séquentielle, comme requête LINQ non parallèle.
AsOrdered Indique que PLINQ doit conserver l’ordre de la séquence source pour le reste de la requête, ou jusqu’à ce que l’ordre soit modifié, par exemple à l’aide d’une clause orderby (Order By en Visual Basic).
AsUnordered Indique que PLINQ ne doit pas conserver l’ordre de la séquence source pour le reste de la requête.
WithCancellation Indique que PLINQ doit régulièrement surveiller l’état du jeton d’annulation fourni et annuler l’exécution si cela est demandé.
WithDegreeOfParallelism Spécifie le nombre maximal de processeurs que PLINQ doit utiliser pour paralléliser la requête.
WithMergeOptions Fournit une indication sur la manière dont PLINQ doit, si possible, fusionner les résultats parallèles en une seule séquence sur le thread utilisé.
WithExecutionMode Indique si PLINQ doit paralléliser la requête même si le comportement par défaut consisterait à l’exécuter de manière séquentielle.
ForAll Méthode d’énumération multithread qui, contrairement à l’itération sur les résultats de la requête, permet leur traitement en parallèle sans nécessiter la fusion préalable dans le thread utilisé.
Aggregate surcharge Surcharge propre à PLINQ qui permet l’agrégation intermédiaire sur des partitions locales des threads,et fonction d’agrégation finale permettant de combiner les résultats de toutes les partitions.

Modèle Opt-in

Lorsque vous écrivez une requête, utilisez PLINQ en appelant la méthode d’extension ParallelEnumerable.AsParallel sur la source de données, comme illustré dans l’exemple suivant.

var source = Enumerable.Range(1, 10000);

// Opt in to PLINQ with AsParallel.
var evenNums = from num in source.AsParallel()
               where num % 2 == 0
               select num;
Console.WriteLine("{0} even numbers out of {1} total",
                  evenNums.Count(), source.Count());
// The example displays the following output:
//       5000 even numbers out of 10000 total
Dim source = Enumerable.Range(1, 10000)

' Opt in to PLINQ with AsParallel
Dim evenNums = From num In source.AsParallel()
               Where num Mod 2 = 0
               Select num
Console.WriteLine("{0} even numbers out of {1} total",
                  evenNums.Count(), source.Count())
' The example displays the following output:
'       5000 even numbers out of 10000 total

La méthode d’extension AsParallel lie les opérateurs de requête suivants, dans ce cas, where et select, aux implémentations System.Linq.ParallelEnumerable.

Modes d’exécution

Par défaut, PLINQ est conservateur. Au moment de l’exécution, l’infrastructure PLINQ analyse la structure globale de la requête. Si la requête est susceptible de produire des accélérations par parallélisation, PLINQ partitionne la séquence source en tâches pouvant être exécutées simultanément. Si la parallélisation d’une requête présente un risque, PLINQ exécute uniquement la requête de manière séquentielle. Si PLINQ a le choix entre un algorithme parallèle potentiellement coûteux ou un algorithme séquentiel abordable, il choisit l’algorithme séquentiel par défaut. Vous pouvez utiliser la méthode WithExecutionMode et l’énumération System.Linq.ParallelExecutionMode pour indiquer à PLINQ de sélectionner l’algorithme parallèle. Cela est utile lorsque vous savez suite à des tests ou des mesures qu’une requête spécifique s’exécute plus rapidement en parallèle. Pour plus d’informations, consultez How to: Specify the Execution Mode in PLINQ (Guide pratique pour spécifier le mode d’exécution dans PLINQ).

Degré de parallélisme

Par défaut, PLINQ utilise tous les processeurs de l’ordinateur hôte. Vous pouvez demander à PLINQ de ne pas utiliser plus d’un nombre spécifié de processeurs à l’aide de la méthode WithDegreeOfParallelism. Cela est utile lorsque vous souhaitez vous assurer que les autres processus en cours d’exécution sur l’ordinateur reçoivent une certaine quantité de temps CPU. L’extrait suivant limite la requête à l’utilisation de deux processeurs maximum.

var query = from item in source.AsParallel().WithDegreeOfParallelism(2)
            where Compute(item) > 42
            select item;
Dim query = From item In source.AsParallel().WithDegreeOfParallelism(2)
            Where Compute(item) > 42
            Select item

Si une requête effectue une quantité importante de travaux non liés au calcul, comme des E/S de fichier, il peut être utile de spécifier un degré de parallélisme supérieur au nombre de cœurs de l’ordinateur.

Comparatif des requêtes parallèles ordonnées et non ordonnées

Dans certaines requêtes, un opérateur de requête doit produire des résultats qui conservent l’ordre de la séquence source. PLINQ fournit l’opérateur AsOrdered à cet effet. AsOrdered est différent de AsSequential. Une séquence AsOrdered est toujours traitée en parallèle, mais ses résultats sont mis en mémoire tampon et triés. Étant donné que la conservation de l’ordre implique généralement un travail supplémentaire, une séquence AsOrdered peut être traitée plus lentement que la séquence AsUnordered par défaut. Le fait qu’une opération parallèle ordonnée de manière spécifique soit plus rapide qu’une version séquentielle de l’opération dépend de nombreux facteurs.

L’exemple de code suivant montre comment utiliser la conservation de l’ordre.

var evenNums =
    from num in numbers.AsParallel().AsOrdered()
    where num % 2 == 0
    select num;
Dim evenNums = From num In numbers.AsParallel().AsOrdered()
               Where num Mod 2 = 0
               Select num


Pour plus d’informations, consultez Order Preservation in PLINQ (Conservation de l’ordre dans PLINQ).

Requêtes parallèles et séquentielles

Certaines opérations requièrent que la source de données soit proposée de manière séquentielle. Les opérateurs de requête ParallelEnumerable basculent automatiquement en mode séquentiel lorsque cela est nécessaire. Pour les opérateurs de requête et les délégués d’utilisateurs définis par l’utilisateur qui nécessitent une exécution séquentielle, PLINQ fournit la méthode AsSequential. Lorsque vous utilisez AsSequential, tous les opérateurs suivants dans la requête sont exécutés séquentiellement jusqu'à ce que AsParallel soit à nouveau appelé. Pour plus d’informations, voir Comment : combiner des requêtes LINQ parallèles et séquentielles.

Options de fusion des résultats de requête

Quand une requête PLINQ s’exécute en parallèle, les résultats issus de chaque thread de travail doivent être refusionnés sur le thread principal pour être utilisés par une boucle foreach (For Each en Visual Basic), ou insérés dans une liste ou un tableau. Dans certains cas, il peut être utile de spécifier un type particulier d’opération de fusion, par exemple, pour commencer à générer des résultats plus rapidement. Pour cela, PLINQ prend en charge la méthode WithMergeOptions et l’énumération ParallelMergeOptions. Pour plus d’informations, consultez l’article Merge Options in PLINQ (Options de fusion de PLINQ).

Opérateur ForAll

Dans les requêtes LINQ séquentielles, l’exécution est différée jusqu’à ce que la requête soit énumérée dans une boucle foreach (For Each en Visual Basic) ou en appelant une méthode telle que ToList, ToArray ou ToDictionary. Dans PLINQ, vous pouvez également utiliser foreach pour exécuter la requête et itérer dans les résultats. Toutefois, foreach lui-même ne s’exécute pas en parallèle, et par conséquent, requiert que les résultats de toutes les tâches parallèles soient refusionnés dans le thread sur lequel la boucle s’exécute. Dans PLINQ, vous pouvez utiliser foreach lorsque vous devez conserver l’ordre final des résultats de requête, et également chaque fois que vous traitez des résultats en série, par exemple, lorsque vous appelez Console.WriteLine pour chaque élément. Pour une exécution plus rapide des requêtes lorsque la conservation de l’ordre n’est pas nécessaire et lorsque le traitement des résultats peut lui-même être parallélisé, utilisez la méthode ForAll pour exécuter une requête PLINQ. ForAll n’effectue pas cette dernière étape de fusion. L'exemple de code suivant montre comment utiliser la méthode ForAll. System.Collections.Concurrent.ConcurrentBag<T> est utilisée ici, car elle est optimisée pour l’ajout simultané de plusieurs threads sans tentative de suppression d’éléments.

var nums = Enumerable.Range(10, 10000);
var query =
    from num in nums.AsParallel()
    where num % 10 == 0
    select num;

// Process the results as each thread completes
// and add them to a System.Collections.Concurrent.ConcurrentBag(Of Int)
// which can safely accept concurrent add operations
query.ForAll(e => concurrentBag.Add(Compute(e)));
Dim nums = Enumerable.Range(10, 10000)
Dim query = From num In nums.AsParallel()
            Where num Mod 10 = 0
            Select num

' Process the results as each thread completes
' and add them to a System.Collections.Concurrent.ConcurrentBag(Of Int)
' which can safely accept concurrent add operations
query.ForAll(Sub(e) concurrentBag.Add(Compute(e)))

L’illustration suivante montre la différence entre foreach et ForAll en ce qui concerne l’exécution des requêtes.

ForAll vs. ForEach

Annulation

PLINQ est intégré aux types d’annulation dans .NET. (Pour plus d’informations, consultez Annulation dans les threads managés.) Par conséquent, contrairement aux requêtes LINQ to Objects séquentielles, les requêtes PLINQ peuvent être annulées. Pour créer une requête PLINQ annulable, utilisez l’opérateur WithCancellation sur la requête et fournissez une instance CancellationToken comme argument. Lorsque la propriété IsCancellationRequested sur le jeton est définie sur true, PLINQ le remarque, arrête le traitement sur tous les threads et lève une OperationCanceledException.

Il est possible qu’une requête PLINQ continue de traiter certains éléments après la définition du jeton d’annulation.

Pour une plus grande réactivité, vous pouvez également répondre aux demandes d’annulation dans les délégués d’utilisateur de longue durée. Pour plus d’informations, consultez How to: Cancel a PLINQ Query (Guide pratique pour annuler une requête PLINQ).

Exceptions

Lorsqu’une requête PLINQ s’exécute, plusieurs exceptions peuvent être générées simultanément à partir de plusieurs threads. En outre, le code destiné à traiter l’exception peut se trouver sur un thread différent de celui du code ayant généré l’exception. PLINQ utilise le type AggregateException afin d’encapsuler toutes les exceptions levées par une requête et de les marshaler à sur le thread appelant. Le thread appelant ne requiert qu’un seul bloc try-catch. Toutefois, vous pouvez itérer sur toutes les exceptions encapsulées dans AggregateException et intercepter celles à partir desquelles vous pouvez effectuer une récupération en toute sécurité. Dans de rares cas, certaines exceptions qui ne sont pas encapsulées dans AggregateException peuvent être levées, et les exceptions ThreadAbortException ne sont pas non plus incluses dans un wrapper.

Lorsque les exceptions sont autorisées à se propager vers le thread lié, il est possible qu'une requête puisse continuer à traiter des éléments après que l'exception ait été levée.

Pour plus d’informations, consultez How to: Handle Exceptions in a PLINQ Query (Comment : traiter des exceptions dans une requête PLINQ).

Partitionneurs personnalisés

Dans certains cas, vous pouvez améliorer les performances des requêtes en écrivant un partitionneur personnalisé qui tire parti de certaines caractéristiques de la source de données. Dans la requête, le partitionneur personnalisé lui-même est l’objet énumérable interrogé.

int[] arr = new int[9999];
Partitioner<int> partitioner = new MyArrayPartitioner<int>(arr);
var query = partitioner.AsParallel().Select(SomeFunction);
Dim arr(10000) As Integer
Dim partitioner As Partitioner(Of Integer) = New MyArrayPartitioner(Of Integer)(arr)
Dim query = partitioner.AsParallel().Select(Function(x) SomeFunction(x))

PLINQ prend en charge un nombre fixe de partitions (bien que les données puissent être réaffectées de manière dynamique à ces partitions pendant l’exécution pour l’équilibrage de charge.). For et ForEach prennent en charge uniquement le partitionnement dynamique, ce qui signifie que le nombre de partitions change en cours d’exécution. Pour plus d’informations, consultez Partitionneurs personnalisés pour PLINQ et la bibliothèque parallèle de tâches (TPL).

Mesure des performances de PLINQ

Dans de nombreux cas, une requête peut être parallélisée, mais la surcharge liée à la configuration de la requête parallèle annule le gain obtenu en termes de performances. Si une requête n’effectue pas beaucoup de calculs ou si la source de données est petite, une requête PLINQ peut être plus lente qu’une requête LINQ to Objects séquentielle. Vous pouvez utiliser l’outil d’analyse des performances parallèles de Visual Studio Team Server pour comparer les performances de diverses requêtes, localiser des goulots d’étranglement et déterminer si votre requête s’exécute en parallèle ou de manière séquentielle. Pour plus d’informations, consultez Visualiseur concurrentiel et How to: Measure PLINQ Query Performance (Comment : mesurer les performances des requêtes PLINQ).

Voir aussi