Partitionneurs personnalisés pour PLINQ et la bibliothèque parallèle de tâches (TPL)Custom Partitioners for PLINQ and TPL

Pour paralléliser une opération sur une source de données, l’une des étapes essentielles consiste à partitionner la source en plusieurs sections accessibles simultanément par plusieurs threads.To parallelize an operation on a data source, one of the essential steps is to partition the source into multiple sections that can be accessed concurrently by multiple threads. PLINQ et la bibliothèque parallèle de tâches (TPL) fournissent des partitionneurs par défaut qui fonctionnent de façon transparente lorsque vous écrivez une requête parallèle ou une boucle ForEach.PLINQ and the Task Parallel Library (TPL) provide default partitioners that work transparently when you write a parallel query or ForEach loop. Pour des scénarios plus élaborés, vous pouvez incorporer votre propre partitionneur.For more advanced scenarios, you can plug in your own partitioner.

Types de partitionnementKinds of Partitioning

Il existe de nombreuses façons de partitionner une source de données.There are many ways to partition a data source. Dans les approches les plus efficaces, plusieurs threads coopèrent pour traiter la séquence source d’origine, au lieu de séparer physiquement la source en plusieurs sous-séquences.In the most efficient approaches, multiple threads cooperate to process the original source sequence, rather than physically separating the source into multiple subsequences. Pour des tableaux et d’autres sources indexées telles que des collections IList où la longueur est connue à l’avance, le partitionnement par plage de valeurs est le type de partitionnement le plus simple.For arrays and other indexed sources such as IList collections where the length is known in advance, range partitioning is the simplest kind of partitioning. Chaque thread reçoit des index de début et de fin uniques pour pouvoir traiter sa plage de la source, sans remplacer ni être remplacé par un autre thread.Every thread receives unique beginning and ending indexes, so that it can process its range of the source without overwriting or being overwritten by any other thread. La seule surcharge impliquée dans le partitionnement par plage de valeurs est le travail initial consistant à créer les plages : aucune synchronisation supplémentaire n’est requise par la suite.The only overhead involved in range partitioning is the initial work of creating the ranges; no additional synchronization is required after that. Par conséquent, cette méthode offre de bonnes performances tant que la charge de travail est répartie uniformément.Therefore, it can provide good performance as long as the workload is divided evenly. L’inconvénient du partitionnement par plage de valeurs réside dans le fait que si un thread se termine plus tôt, il ne peut pas aider les autres threads à terminer leur travail.A disadvantage of range partitioning is that if one thread finishes early, it cannot help the other threads finish their work.

Pour les listes liées ou d’autres collections dont la longueur n’est pas connue, vous pouvez utiliser le partitionnement par segments.For linked lists or other collections whose length is not known, you can use chunk partitioning. Dans le partitionnement par segments, chaque thread ou tâche d’une boucle parallèle ou d’une requête consomme un certain nombre d’éléments de la source dans un segment, les traite, puis revient pour extraire des éléments supplémentaires.In chunk partitioning, every thread or task in a parallel loop or query consumes some number of source elements in one chunk, processes them, and then comes back to retrieve additional elements. Le partitionneur s’assure que tous les éléments sont distribués et qu’il n’y a pas de doublons.The partitioner ensures that all elements are distributed and that there are no duplicates. Un segment peut être de n’importe quelle taille.A chunk may be any size. Par exemple, le partitionneur présenté dans la section Guide pratique pour implémenter des partitions dynamiques crée des segments qui ne contiennent qu’un seul élément.For example, the partitioner that is demonstrated in How to: Implement Dynamic Partitions creates chunks that contain just one element. Tant que les segments ne sont pas trop volumineux, ce type de partitionnement effectue, par nature, un équilibrage de charge car l’affectation des éléments aux threads n’est pas prédéfinie.As long as the chunks are not too large, this kind of partitioning is inherently load-balancing because the assignment of elements to threads is not pre-determined. Cependant, le partitionneur déclenche la surcharge de synchronisation chaque fois que le thread a besoin d’un autre segment.However, the partitioner does incur the synchronization overhead each time the thread needs to get another chunk. Le niveau de synchronisation effectué dans ce cas est inversement proportionnel à la taille des segments.The amount of synchronization incurred in these cases is inversely proportional to the size of the chunks.

En règle générale, le partitionnement par plage de valeurs n’est plus rapide que lorsque la durée d’exécution du délégué est faible à modérée, que la source comporte un grand nombre d’éléments, et que le travail total de chaque partition est à peu près équivalent.In general, range partitioning is only faster when the execution time of the delegate is small to moderate, and the source has a large number of elements, and the total work of each partition is roughly equivalent. Le partitionnement par segments est donc généralement plus rapide dans la plupart des cas.Chunk partitioning is therefore generally faster in most cases. Sur les sources comportant un petit nombre d’éléments ou avec des durées d’exécution plus longues pour le délégué, les performances du partitionnement par plage de valeurs et du partitionnement par segments sont équivalentes.On sources with a small number of elements or longer execution times for the delegate, then the performance of chunk and range partitioning is about equal.

Les partitionneurs TPL prennent également en charge un nombre dynamique de partitions.The TPL partitioners also support a dynamic number of partitions. Cela signifie qu’ils peuvent créer des partitions à la volée, par exemple lorsque la boucle ForEach génère une nouvelle tâche.This means they can create partitions on-the-fly, for example, when the ForEach loop spawns a new task. Cette fonctionnalité permet la mise à l’échelle du partitionneur par rapport à la boucle elle-même.This feature enables the partitioner to scale together with the loop itself. Par nature, les partitionneurs dynamiques effectuent également un équilibrage de charge.Dynamic partitioners are also inherently load-balancing. Lorsque vous créez un partitionneur personnalisé, vous devez faire en sortie que le partitionnement dynamique soit utilisable par une boucle ForEach.When you create a custom partitioner, you must support dynamic partitioning to be consumable from a ForEach loop.

Configuration des partitionneurs d’équilibrage de charge pour PLINQConfiguring Load Balancing Partitioners for PLINQ

Certaines surcharges de la méthode Partitioner.Create vous permettent de créer un partitionneur pour un tableau ou une source IList et de spécifier s’il doit tenter d’équilibrer la charge de travail entre les threads.Some overloads of the Partitioner.Create method let you create a partitioner for an array or IList source and specify whether it should attempt to balance the workload among the threads. Lorsque le partitionneur est configuré pour l’équilibrage de charge, le partitionnement par segments est utilisé, et les éléments sont transmis, à la demande, par petits segments à chaque partition.When the partitioner is configured to load-balance, chunk partitioning is used, and the elements are handed off to each partition in small chunks as they are requested. Cette approche garantit que toutes les partitions disposent d’éléments à traiter jusqu'à ce que la boucle ou la requête soit terminée.This approach helps ensure that all partitions have elements to process until the entire loop or query is completed. Une surcharge supplémentaire peut être utilisée pour fournir le partitionnement d’équilibrage de charge de n’importe quelle source IEnumerable.An additional overload can be used to provide load-balancing partitioning of any IEnumerable source.

En général, l’équilibrage de charge requiert que les partitions sollicitent fréquemment le partitionneur pour obtenir des éléments.In general, load balancing requires the partitions to request elements relatively frequently from the partitioner. En revanche, un partitionneur qui effectue un partitionnement statique peut affecter les éléments à chaque partitionneur en même temps à l’aide d’un partitionnement par plage de valeurs ou par segments.By contrast, a partitioner that does static partitioning can assign the elements to each partitioner all at once by using either range or chunk partitioning. Cette méthode nécessite moins de surcharge que l’équilibrage de charge, mais elle peut prendre plus de temps à s’exécuter si un thread se retrouve avec beaucoup plus de travail que les autres.This requires less overhead than load balancing, but it might take longer to execute if one thread ends up with significantly more work than the others. Par défaut, lorsqu’il reçoit un objet IList ou un tableau, PLINQ utilise toujours le partitionnement par plage de valeurs sans équilibrage de charge.By default when it is passed an IList or an array, PLINQ always uses range partitioning without load balancing. Pour activer l’équilibrage de charge pour PLINQ, utilisez la méthode Partitioner.Create, comme indiqué dans l’exemple suivant.To enable load balancing for PLINQ, use the Partitioner.Create method, as shown in the following example.

// Static partitioning requires indexable source. Load balancing
// can use any IEnumerable.
var nums = Enumerable.Range(0, 100000000).ToArray();

// Create a load-balancing partitioner. Or specify false for static partitioning.
Partitioner<int> customPartitioner = Partitioner.Create(nums, true);

// The partitioner is the query's data source.
var q = from x in customPartitioner.AsParallel()
        select x * Math.PI;

q.ForAll((x) =>
{
    ProcessData(x);
});
' Static number of partitions requires indexable source.
Dim nums = Enumerable.Range(0, 100000000).ToArray()

' Create a load-balancing partitioner. Or specify false For  Shared partitioning.
Dim customPartitioner = Partitioner.Create(nums, True)

' The partitioner is the query's data source.
Dim q = From x In customPartitioner.AsParallel()
        Select x * Math.PI

q.ForAll(Sub(x) ProcessData(x))

La meilleure méthode pour déterminer si vous devez utiliser l’équilibrage de charge dans un scénario donné consiste à faire des essais et à mesurer la durée des opérations avec des charges et des configurations d’ordinateur représentatives.The best way to determine whether to use load balancing in any given scenario is to experiment and measure how long it takes operations to complete under representative loads and computer configurations. Par exemple, le partitionnement statique peut accélérer considérablement les opérations sur un ordinateur multicœur doté de quelques cœurs, mais il peut entraîner des ralentissements sur les ordinateurs qui disposent de nombreux cœurs.For example, static partitioning might provide significant speedup on a multi-core computer that has only a few cores, but it might result in slowdowns on computers that have relatively many cores.

Le tableau ci-dessous répertorie les surcharges disponibles avec la méthode Create.The following table lists the available overloads of the Create method. L’utilisation de ces partitionneurs ne se limite pas à PLINQ ou à Task.These partitioners are not limited to use only with PLINQ or Task. Ils peuvent également être utilisés avec n’importe quelle construction parallèle personnalisée.They can also be used with any custom parallel construct.

SurchargeOverload Utiliser l'équilibrage de chargeUses load balancing
Create<TSource>(IEnumerable<TSource>) ToujoursAlways
Create<TSource>(TSource[], Boolean) Lorsque l’argument booléen est spécifié comme trueWhen the Boolean argument is specified as true
Create<TSource>(IList<TSource>, Boolean) Lorsque l’argument booléen est spécifié comme trueWhen the Boolean argument is specified as true
Create(Int32, Int32) JamaisNever
Create(Int32, Int32, Int32) JamaisNever
Create(Int64, Int64) JamaisNever
Create(Int64, Int64, Int64) JamaisNever

Configuration de partitionneurs de plages statiques pour Parallel.ForEachConfiguring Static Range Partitioners for Parallel.ForEach

Dans une boucle For, le corps de la boucle est transmis à la méthode en tant que délégué.In a For loop, the body of the loop is provided to the method as a delegate. Le coût d’un appel à ce délégué est équivalent à celui d’un appel à une méthode virtuelle.The cost of invoking that delegate is about the same as a virtual method call. Dans certains scénarios, le corps d’une boucle parallèle peut être suffisamment petit de sorte que le coût d’un appel au délégué sur chaque itération de la boucle devient important.In some scenarios, the body of a parallel loop might be small enough that the cost of the delegate invocation on each loop iteration becomes significant. Dans ce cas, vous pouvez utiliser une des surcharges Create pour créer un objet IEnumerable<T> de partitions par plages de valeurs sur les éléments sources.In such situations, you can use one of the Create overloads to create an IEnumerable<T> of range partitions over the source elements. Vous pouvez ensuite transmettre cette collection de plages de valeurs à une méthode ForEach dont le corps se compose d’une boucle for standard.Then, you can pass this collection of ranges to a ForEach method whose body consists of a regular for loop. L’avantage de cette approche est que le coût d’un appel au délégué n’est généré qu’une seule fois par plage au lieu d’une seule fois par élément.The benefit of this approach is that the delegate invocation cost is incurred only once per range, rather than once per element. L'exemple suivant illustre le modèle de base.The following example demonstrates the basic pattern.

using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static void Main()
    {

        // Source must be array or IList.
        var source = Enumerable.Range(0, 100000).ToArray();

        // Partition the entire source array.
        var rangePartitioner = Partitioner.Create(0, source.Length);

        double[] results = new double[source.Length];

        // Loop over the partitions in parallel.
        Parallel.ForEach(rangePartitioner, (range, loopState) =>
        {
            // Loop over each range element without a delegate invocation.
            for (int i = range.Item1; i < range.Item2; i++)
            {
                results[i] = source[i] * Math.PI;
            }
        });

        Console.WriteLine("Operation complete. Print results? y/n");
        char input = Console.ReadKey().KeyChar;
        if (input == 'y' || input == 'Y')
        {
            foreach(double d in results)
            {
                Console.Write("{0} ", d);
            }           
        }
    }
}
Imports System.Threading.Tasks
Imports System.Collections.Concurrent

Module PartitionDemo

    Sub Main()
        ' Source must be array or IList.
        Dim source = Enumerable.Range(0, 100000).ToArray()

        ' Partition the entire source array. 
        ' Let the partitioner size the ranges.
        Dim rangePartitioner = Partitioner.Create(0, source.Length)

        Dim results(source.Length - 1) As Double

        ' Loop over the partitions in parallel. The Sub is invoked
        ' once per partition.
        Parallel.ForEach(rangePartitioner, Sub(range, loopState)

                                               ' Loop over each range element without a delegate invocation.
                                               For i As Integer = range.Item1 To range.Item2 - 1
                                                   results(i) = source(i) * Math.PI
                                               Next
                                           End Sub)
        Console.WriteLine("Operation complete. Print results? y/n")
        Dim input As Char = Console.ReadKey().KeyChar
        If input = "y"c Or input = "Y"c Then
            For Each d As Double In results
                Console.Write("{0} ", d)
            Next
        End If

    End Sub
End Module

Chaque thread de la boucle reçoit son propre objet Tuple<T1,T2>, qui contient les valeurs d’index de début et de fin dans la sous-plage spécifiée.Every thread in the loop receives its own Tuple<T1,T2> that contains the starting and ending index values in the specified sub-range. La boucle for interne utilise les valeurs fromInclusive et toExclusive pour parcourir le tableau ou IList directement.The inner for loop uses the fromInclusive and toExclusive values to loop over the array or the IList directly.

Une des surcharges Create vous permet de spécifier la taille des partitions ainsi que leur nombre.One of the Create overloads lets you specify the size of the partitions, and the number of partitions. Cette surcharge peut être utilisée dans des scénarios où le travail par élément est si faible que même un appel à une méthode virtuelle par élément a un impact perceptible sur les performances.This overload can be used in scenarios where the work per element is so low that even one virtual method call per element has a noticeable impact on performance.

Partitionneurs personnalisésCustom Partitioners

Dans certains scénarios, il peut être utile ou même obligatoire d’implémenter votre propre partitionneur.In some scenarios, it might be worthwhile or even required to implement your own partitioner. Par exemple, vous pouvez partionner une classe de collection personnalisée plus efficacement qu’avec les partitionneurs par défaut, selon votre connaissance de la structure interne de la classe.For example, you might have a custom collection class that you can partition more efficiently than the default partitioners can, based on your knowledge of the internal structure of the class. Ou vous pouvez créer des partitions de plages de valeurs de tailles différentes en fonction du temps nécessaire pour traiter les éléments situés en différents emplacements de la collection source.Or, you may want to create range partitions of varying sizes based on your knowledge of how long it will take to process elements at different locations in the source collection.

Pour créer un partitionneur personnalisé de base, dérivez une classe de System.Collections.Concurrent.Partitioner<TSource> puis remplacez les méthodes virtuelles, comme décrit dans le tableau suivant.To create a basic custom partitioner, derive a class from System.Collections.Concurrent.Partitioner<TSource> and override the virtual methods, as described in the following table.

GetPartitions Cette méthode est appelée une fois par le thread principal et retourne un objet IList(IEnumerator(TSource)).This method is called once by the main thread and returns an IList(IEnumerator(TSource)). Chaque thread de travail dans la boucle ou la requête peut appeler GetEnumerator sur la liste pour récupérer un objet IEnumerator<T> sur une partition distincte.Each worker thread in the loop or query can call GetEnumerator on the list to retrieve a IEnumerator<T> over a distinct partition.
SupportsDynamicPartitions Retournez true si vous implémentez GetDynamicPartitions, et false dans le cas contraire.Return true if you implement GetDynamicPartitions, otherwise, false.
GetDynamicPartitions Si SupportsDynamicPartitions est true, cette méthode peut éventuellement être appelée à la place de GetPartitions.If SupportsDynamicPartitions is true, this method can optionally be called instead of GetPartitions.

Si les résultats doivent pouvoir être triés ou que vous avez besoin d’un accès indexé aux éléments, dérivez System.Collections.Concurrent.OrderablePartitioner<TSource> et remplacez ses méthodes virtuelles, comme décrit dans le tableau suivant.If the results must be sortable or you require indexed access into the elements, then derive from System.Collections.Concurrent.OrderablePartitioner<TSource> and override its virtual methods as described in the following table.

GetPartitions Cette méthode est appelée une fois par le thread principal et retourne un objet IList(IEnumerator(TSource)).This method is called once by the main thread and returns an IList(IEnumerator(TSource)). Chaque thread de travail dans la boucle ou la requête peut appeler GetEnumerator sur la liste pour récupérer un objet IEnumerator<T> sur une partition distincte.Each worker thread in the loop or query can call GetEnumerator on the list to retrieve a IEnumerator<T> over a distinct partition.
SupportsDynamicPartitions Retournez true si vous implémentez GetDynamicPartitions, et false dans le cas contraire.Return true if you implement GetDynamicPartitions; otherwise, false.
GetDynamicPartitions En règle générale, cette méthode appelle simplement GetOrderableDynamicPartitions.Typically, this just calls GetOrderableDynamicPartitions.
GetOrderableDynamicPartitions Si SupportsDynamicPartitions est true, cette méthode peut éventuellement être appelée à la place de GetPartitions.If SupportsDynamicPartitions is true, this method can optionally be called instead of GetPartitions.

Le tableau suivant fournit des détails supplémentaires sur la façon dont les trois types de partitionneurs d’équilibrage de charge implémentent la classe OrderablePartitioner<TSource>.The following table provides additional details about how the three kinds of load-balancing partitioners implement the OrderablePartitioner<TSource> class.

Méthode/propriétéMethod/Property IList / tableau sans équilibrage de chargeIList / Array without Load Balancing IList / tableau avec équilibrage de chargeIList / Array with Load Balancing IEnumerableIEnumerable
GetOrderablePartitions Utilise le partitionnement par plages de valeursUses range partitioning Utilise le partitionnement par segments, optimisé pour les listes, pour la valeur partitionCount spécifiéeUses chunk partitioning optimized for Lists for the partitionCount specified Utilise le partitionnement par segments en créant un nombre statique de partitions.Uses chunk partitioning by creating a static number of partitions.
OrderablePartitioner<TSource>.GetOrderableDynamicPartitions Lève une exception non prise en chargeThrows not-supported exception Utilise le partitionnement par segments, optimisé pour les listes, pour les partitions dynamiquesUses chunk partitioning optimized for Lists and dynamic partitions Utilise le partitionnement par segments en créant un nombre dynamique de partitions.Uses chunk partitioning by creating a dynamic number of partitions.
KeysOrderedInEachPartition Retourne true.Returns true Retourne true.Returns true Retourne true.Returns true
KeysOrderedAcrossPartitions Retourne true.Returns true Retourne false.Returns false Retourne false.Returns false
KeysNormalized Retourne true.Returns true Retourne true.Returns true Retourne true.Returns true
SupportsDynamicPartitions Retourne false.Returns false Retourne true.Returns true Retourne true.Returns true

Partitions dynamiquesDynamic Partitions

Si vous envisagez d’utiliser le partitionneur dans une méthode ForEach, vous devez être en mesure de retourner un nombre dynamique de partitions.If you intend the partitioner to be used in a ForEach method, you must be able to return a dynamic number of partitions. Cela signifie que le partitionneur peut fournir un énumérateur pour une nouvelle partition, à la demande et à tout moment pendant l’exécution de la boucle.This means that the partitioner can supply an enumerator for a new partition on-demand at any time during loop execution. En fait, chaque fois que la boucle ajoute une nouvelle tâche parallèle, elle demande une nouvelle partition pour cette tâche.Basically, whenever the loop adds a new parallel task, it requests a new partition for that task. Si vous avez besoin de pouvoir classer les données, effectuez une dérivation System.Collections.Concurrent.OrderablePartitioner<TSource> afin d’affecter un index unique à chaque élément de chaque partition.If you require the data to be orderable, then derive from System.Collections.Concurrent.OrderablePartitioner<TSource> so that each item in each partition is assigned a unique index.

Pour plus d’informations et consulter un exemple, voir Guide pratique pour implémenter des partitions dynamiques.For more information, and an example, see How to: Implement Dynamic Partitions.

Contrat pour les partitionneursContract for Partitioners

Lorsque vous implémentez un partitionneur personnalisé, suivez ces instructions pour garantir une interaction correcte avec PLINQ et ForEach dans la bibliothèque parallèle de tâches (TPL) :When you implement a custom partitioner, follow these guidelines to help ensure correct interaction with PLINQ and ForEach in the TPL:

  • Si GetPartitions est appelé avec un argument de zéro ou moins pour partitionsCount, levez ArgumentOutOfRangeException.If GetPartitions is called with an argument of zero or less for partitionsCount, throw ArgumentOutOfRangeException. Bien que PLINQ et TPL ne fourniront jamais une valeur partitionCount égale à 0, nous vous recommandons néanmoins de vous prémunir contre ce risque.Although PLINQ and TPL will never pass in a partitionCount equal to 0, we nevertheless recommend that you guard against the possibility.

  • GetPartitions et GetOrderablePartitions devraient toujours renvoyer un nombre de partitions équivalant à partitionsCount.GetPartitions and GetOrderablePartitions should always return partitionsCount number of partitions. Si le partitionneur manque de données et ne peut pas créer autant de partitions que demandé, la méthode devrait retourner un énumérateur vide pour chacune des partitions restantes.If the partitioner runs out of data and cannot create as many partitions as requested, then the method should return an empty enumerator for each of the remaining partitions. Sinon, PLINQ et TPL lèveront une InvalidOperationException.Otherwise, both PLINQ and TPL will throw an InvalidOperationException.

  • GetPartitions, GetOrderablePartitions, GetDynamicPartitions, et GetOrderableDynamicPartitions ne devraient jamais retourner null (Nothing en Visual Basic).GetPartitions, GetOrderablePartitions, GetDynamicPartitions, and GetOrderableDynamicPartitions should never return null (Nothing in Visual Basic). Si tel est le cas, PLINQ/TPL lèveront une InvalidOperationException.If they do, PLINQ / TPL will throw an InvalidOperationException.

  • Les méthodes qui retournent des partitions devraient toujours renvoyer des partitions capables d’énumérer totalement et de manière unique la source de données.Methods that return partitions should always return partitions that can fully and uniquely enumerate the data source. Il ne devrait y avoir aucune duplication dans la source de données ou les éléments ignorés, sauf si cela est spécifiquement requis par la conception du partitionneur.There should be no duplication in the data source or skipped items unless specifically required by the design of the partitioner. Si cette règle n’est pas suivie, l’ordre de sortie peut être brouillé.If this rule is not followed, then the output order may be scrambled.

  • Les accesseurs booléens suivants doivent toujours retourner correctement les valeurs ci-dessous afin que l’ordre de sortie ne soit pas brouillé :The following Boolean getters must always accurately return the following values so that the output order is not scrambled:

    • KeysOrderedInEachPartition : chaque partition retourne des éléments en augmentant les index clés.KeysOrderedInEachPartition: Each partition returns elements with increasing key indices.

    • KeysOrderedAcrossPartitions : pour toutes les partitions retournées, les index clés dans la partition i sont plus élevés que les index clés de la partition i-1.KeysOrderedAcrossPartitions: For all partitions that are returned, the key indices in partition i are higher than the key indices in partition i-1.

    • KeysNormalized: tous les index clés augmentent de monotone, sans écarts, à partir de zéro.KeysNormalized: All key indices are monotonically increasing without gaps, starting from zero.

  • Tous les index doivent être uniques.All indices must be unique. Il ne doit pas y avoir de doublons.There may not be duplicate indices. Si cette règle n’est pas suivie, l’ordre de sortie peut être brouillé.If this rule is not followed, then the output order may be scrambled.

  • Tous les index doivent être non négatifs.All indices must be nonnegative. Si cette règle n’est pas suivie, PLINQ/TPL peuvent lever des exceptions.If this rule is not followed, then PLINQ/TPL may throw exceptions.

Voir aussiSee also