Partager via


Introduction à PLINQ

Qu'est-ce qu'une requête parallèle ?

LINQ (Language Integrated Query) a été introduit dans le .NET Framework version 3.0. Il comprend un modèle unifié pour les requêtes de sources de données System.Collections.IEnumerable or System.Collections.Generic.IEnumerable<T> d'une façon sécurisée du point de vue du type. 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 comprenez les notions fondamentales de LINQ. Pour plus d'informations, consultez LINQ (Language-Integrated Query).

PLINQ (Parallel LINQ) est une implémentation parallèle du modèle LINQ. Une requête PLINQ ressemble à bien des égards à une requête non parallèle LINQ to Objects. Les requêtes PLINQ, tout comme les requêtes séquentielles LINQ, fonctionnent avec toute source 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 n'est pas énumérée. La différence principale est que PLINQ essaie d'utiliser pleinement tous les processeurs du système. Pour cela, il partitionne la source de données en segments et exécute la requête sur chaque segment sur des threads de travail différents en parallèle sur plusieurs processeurs. L'exécution parallèle signifie souvent que la requête s'exécute beaucoup plus vite.

Via l'exécution parallèle, PLINQ peut accomplir une amélioration importante des performances du code hérité de certains genres de requêtes, souvent en ajoutant simplement 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 exécutées ne sont pas plus rapides dans PLINQ. En réalité, la parallélisation ralentit certaines requêtes. Par conséquent, vous devez comprendre comment les aspects, tels que le classement, affectent les requêtes parallèles. Pour plus d'informations, consultez Fonctionnement de l'accélération dans PLINQ.

RemarqueRemarque

Cette documentation utilise des expressions lambda pour définir des délégués en PLINQ.Si vous n'êtes pas familiarisé avec les expressions lambda en C# ou Visual Basic, consultez 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 d'autres d'exemples de code et informations détaillées.

Classe ParallelEnumerable

La classe System.Linq.ParallelEnumerable comprend la plupart des fonctionnalités de PLINQ. Cette classe et les autres types d'espace de noms System.Linq sont compilés dans l'assembly System.Core.dll. Les projets par défaut C# et Visual Basic dans Visual Studio référencent l'assembly et importent l'espace de noms.

ParallelEnumerable inclut des implémentations de tous les opérateurs de requête standard pris en charge par LINQ to Objects, mais n'essaie pas de les paralléliser. Si vous n'êtes pas familiarisé avec LINQ, consultez Introduction à LINQ.

En plus des opérateurs de requête standard, la classe ParallelEnumerable contient un ensemble de méthodes permettant certains comportements spécifiques à l'exécution parallèle. Ces méthodes spécifiques à PLINQ sont répertoriées dans le tableau suivant.

Opérateur ParallelEnumerable

Description

AsParallel

Point d'entrée de PLINQ. Spécifie que le reste de la requête doit être parallélisé, si possible.

AsSequential<TSource>

Spécifie que le reste de la requête doit être exécuté séquentiellement, comme une requête LINQ non parallèle.

AsOrdered

Spécifie que PLINQ doit conserver le classement de la séquence source pour le reste de la requête ou jusqu'à ce que le classement soit modifié, par exemple par l'utilisation d'une clause orderby (Order By en Vlsual Basic).

AsUnordered<TSource>

Spécifie que, pour le reste de la requête, PLINQ n'a pas besoin de conserver le classement de la séquence source.

WithCancellation<TSource>

Spécifie que PLINQ doit régulièrement surveiller l'état du jeton d'annulation fourni et annuler l'exécution si cela est demandé.

WithDegreeOfParallelism<TSource>

Spécifie le nombre maximal de processeurs que PLINQ doit utiliser pour paralléliser la requête.

WithMergeOptions<TSource>

Fournit un commentaire sur la manière dont PLINQ doit, si possible, fusionner des résultats parallèles en une seule séquence sur le thread consommateur.

WithExecutionMode<TSource>

Spécifie si PLINQ doit paralléliser la requête même quand le comportement par défaut est d'exécuter séquentiellement.

ForAll<TSource>

Méthode d'énumération multithread qui, au lieu d'itérer au sein des résultats de la requête, permet aux résultats d'être traités en parallèle sans être d'abord fusionnés sur le thread consommateur.

Surcharge Aggregate

Surcharge unique à PLINQ qui permet l'agrégation intermédiaire sur des partitions locales de thread, plus une fonction d'agrégation finale pour combiner les résultats de toutes les partitions.

Modèle déclaratif

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

Dim source = Enumerable.Range(1, 10000)

' Opt in to PLINQ with AsParallel
Dim evenNums = From num In source.AsParallel()
               Where Compute(num) > 0
               Select num
var source = Enumerable.Range(1, 10000);


// Opt-in to PLINQ with AsParallel
var evenNums = from num in source.AsParallel()
               where Compute(num) > 0
               select num;

La méthode d'extension AsParallel lie les opérateurs de requête suivants, en l'occurrence, 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 totale de la requête. Si la requête est susceptible de s'accélérer grâce à la parallélisation, PLINQ partitionne la séquence source en tâches pouvant être exécutées simultanément. S'il est risqué de paralléliser une requête, PLINQ exécute la requête séquentiellement. Si PLINQ a le choix entre un algorithme parallèle potentiellement coûteux et un algorithme séquentiel non coûteux, il choisit par défaut l'algorithme séquentiel. Vous pouvez utiliser la méthode WithExecutionMode<TSource> et l'énumération System.Linq.ParallelExecutionMode pour indiquer à PLINQ de sélectionner l'algorithme parallèle. Cela se révèle utile lorsque vous savez, grâce aux tests et mesures effectuées, qu'une requête particulière s'exécute plus rapidement en parallèle. Pour plus d'informations, consultez Comment : spécifier le mode d'exécution en PLINQ.

Degré de parallélisme

Par défaut, PLINQ utilise un maximum de 64 processeurs sur l'ordinateur hôte. Vous pouvez indiquer à PLINQ d'utiliser un nombre maximal de processeurs à l'aide de la méthode WithDegreeOfParallelism<TSource>. Cela se révèle utile lorsque vous voulez vous assurer que les autres processus qui s'exécutent sur l'ordinateur reçoivent une certaine quantité de temps processeur. L'extrait de code suivant limite la requête à l'utilisation d'un maximum de deux processeurs.

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

Dans les cas où une requête exécute une quantité significative de travail non orienté ordinateur, tel que l'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.

Requêtes parallèles classées et non classées

Dans certaines requêtes, un opérateur de requête doit produire des résultats qui conservent le classement de la séquence source. Pour cela, PLINQ fournit l'opérateur AsOrdered. AsOrdered est différent de AsSequential<TSource>. 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 en général un travail supplémentaire, une séquence AsOrdered peut être traitée plus lentement que la séquence AsUnordered<TSource> par défaut. L'exécution d'une opération parallèle classée n'est pas toujours plus rapide que l'exécution séquentielle car de nombreux facteurs entrent en jeu.

L'exemple de code suivant indique comment déclarer la conservation de l'ordre.

        Dim evenNums = From num In numbers.AsParallel().AsOrdered()
                      Where num Mod 2 = 0
                      Select num



            evenNums = from num in numbers.AsParallel().AsOrdered()
                       where num % 2 == 0
                       select num;


Pour plus d'informations, consultez Conservation de l'ordre en PLINQ.

Requêtes parallèles etrequêtes séquentielles

Certaines opérations requièrent que les données sources soient remises de façon séquentielle. Les opérateurs de requête ParallelEnumerable retournent automatiquement au mode séquentiel si nécessaire. Pour les opérateurs de requête et délégués utilisateurs définis par l'utilisateur qui nécessitent une exécution séquentielle, PLINQ fournit la méthode AsSequential<TSource>. Lorsque vous utilisez AsSequential<TSource>, tous les opérateurs suivants de la requête sont exécutés séquentiellement jusqu'à ce que AsParallel soit appelé de nouveau. Pour plus d'informations, consultez Comment : combiner des requêtes LINQ parallèles et séquentielles.

Options de fusion des résultats de requête

Lorsqu'une requête PLINQ s'exécute en parallèle, ses résultats de chaque thread de travail doivent être fusionnés sur le thread principal pour être consommé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 bénéfique de spécifier un genre particulier d'opération de fusion, par exemple, pour commencer à produire des résultats plus rapidement. Pour cela, PLINQ prend en charge la méthode WithMergeOptions<TSource> et l'énumération ParallelMergeOptions. Pour plus d'informations, consultez Options de fusion en 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 foreach ToList<TSource>, ToTSource> ou ToDictionary. Dans PLINQ, vous pouvez également utiliser foreach pour exécuter la requête et itérer au sein des résultats. Toutefois, foreach ne s'exécute pas en parallèle, et par conséquent, requiert que la sortie de toutes les tâches parallèles soit fusionnée dans le thread sur lequel la boucle s'exécute. En PLINQ, vous pouvez utiliser foreach lorsque le classement final des résultats de la requête doit être conservé et chaque fois que vous traitez des résultats séquentiellement, par exemple lorsque vous appelez Console.WriteLine pour chaque élément. Pour une exécution de requête plus rapide lorsque la conservation de l'ordre n'est pas obligatoire et lorsque le traitement des résultats peut être parallélisé, utilisez la méthode ForAll<TSource> pour exécuter une requête PLINQ. ForAll<TSource> n'exécute pas cette dernière étape de fusion. L'exemple de code suivant indique comment utiliser la méthode ForAll<TSource>. System.Collections.Concurrent.ConcurrentBag<T> est utilisée ici car elle est optimisée pour plusieurs threads qui effectuent un ajout simultané sans essayer de supprimer des éléments.

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)))

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)));

L'illustration suivante montre la différence entre foreach et ForAll<TSource> quant à exécution de la requête.

ForAll et ForEach

Annulation

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

Une requête PLINQ peut continuer à traiter des éléments après la définition du jeton d'annulation.

Pour une plus grande réactivité, vous pouvez également répondre aux requêtes d'annulation dans des délégués utilisateurs de longue durée. Pour plus d'informations, consultez Comment : annuler une requête PLINQ.

Exceptions

Lorsqu'une requête PLINQ s'exécute, plusieurs exceptions peuvent être levées en même temps par des threads différents. Le code servant à gérer l'exception peut se trouver sur un thread différent que le code qui a levé l'exception. PLINQ utilise le type AggregateException pour encapsuler toutes les exceptions levées par une requête et marshale ces exceptions sur le thread appelant. Sur le thread appelant, un seul bloc try-catch est nécessaire. Toutefois, vous pouvez itérer au sein de toutes les exceptions encapsulées dans AggregateException et intercepter celles dont la récupération est sans risque. Dans de rares cas, les exceptions non encapsulées dans un AggregateException et les exceptions ThreadAbortException également non encapsulées peuvent être levées.

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

Pour plus d'informations, consultez Comment : gérer des exceptions dans une requête PLINQ.

Partitionneurs personnalisés

Dans certains cas, il est possible d'améliorer les performances des requêtes en écrivant un partitionneur personnalisé qui tire parti de certaines caractéristiques des données sources. Dans la requête, le partitionneur personnalisé est l'objet énumérable faisant l'objet de la requête.

    [Visual Basic]
    Dim arr(10000) As Integer
    Dim partitioner = New MyArrayPartitioner(Of Integer)(arr)
    Dim query = partitioner.AsParallel().Select(Function(x) SomeFunction(x))
    [C#]
    int[] arr= ...;
    Partitioner<int> partitioner = newMyArrayPartitioner<int>(arr);
    var q = partitioner.AsParallel().Select(x => SomeFunction(x));

PLINQ prend en charge un nombre fixe de partitions (bien que les données puissent être réaffectées dynamiquement à ces partitions pendant l'exécution de l'équilibrage de charge). For et ForEach prennent uniquement en charge le partitionnement dynamique, ce qui signifie que le nombre de partitions change au moment de l'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 PLINQ

Dans de nombreux cas, une requête peut être parallélisée, mais les surcharges de la configuration de la requête parallèle l'emportent sur les performances gagnées. Si une requête n'exécute pas beaucoup de calculs ou si la source de données est petite, une requête PLINQ peut se révéler plus lente qu'une requête LINQ to Objects séquentielle. Vous pouvez utiliser l'analyseur de performances parallèles de Visual Studio Team Server pour comparer les performances des différentes requêtes, trouver des goulots d'étranglement du processeur et déterminer si votre requête s'exécute en parallèle ou séquentiellement. Pour plus d'informations, consultez Visualiseur concurrence et Comment : mesurer les performances de requêtes PLINQ.

Voir aussi

Concepts

Parallel LINQ (PLINQ)

Fonctionnement de l'accélération dans PLINQ