Partitioner personalizzati per PLINQ e TPLCustom Partitioners for PLINQ and TPL

Per parallelizzare un'operazione in un'origine dati, è essenziale partizionare l'origine in più sezioni a cui è possibile accedere contemporaneamente da più thread.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 e Task Parallel Library (TPL) forniscono partitioner predefiniti che funzionano in modo trasparente quando si scrive una query o un ciclo ForEach parallelo.PLINQ and the Task Parallel Library (TPL) provide default partitioners that work transparently when you write a parallel query or ForEach loop. Per scenari più avanzati, è possibile collegare il proprio partitioner.For more advanced scenarios, you can plug in your own partitioner.

Tipi di partizionamentoKinds of Partitioning

Esistono diversi modi per partizionare un'origine dati.There are many ways to partition a data source. Negli approcci più efficienti, più thread cooperano per elaborare la sequenza di origine originale, invece di separare fisicamente l'origine in più sottosequenze.In the most efficient approaches, multiple threads cooperate to process the original source sequence, rather than physically separating the source into multiple subsequences. Per le matrici e le altre origini indicizzate, ad esempio le raccolte IList in cui la lunghezza è nota in anticipo, il partizionamento per intervalli è il tipo più semplice di partizionamento.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. Ogni thread riceve indici iniziali e finali univoci, in modo che possa elaborare l'intervallo dell'origine senza sovrascrivere un altro thread o esserne sovrascritto.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. L'unico sovraccarico che si verifica nel partizionamento per intervalli è dovuto all'attività iniziale di creazione degli intervalli, dopo la quale non sono necessarie altre operazioni di sincronizzazione.The only overhead involved in range partitioning is the initial work of creating the ranges; no additional synchronization is required after that. Può quindi offrire buone prestazioni purché il carico di lavoro sia suddiviso equamente.Therefore, it can provide good performance as long as the workload is divided evenly. Uno svantaggio del partizionamento per intervalli è che, se un thread termina prima del previsto, non può supportare il completamento degli altri thread.A disadvantage of range partitioning is that if one thread finishes early, it cannot help the other threads finish their work.

Per gli elenchi collegati o altre raccolte la cui lunghezza non è nota, è possibile usare il partizionamento in blocchi.For linked lists or other collections whose length is not known, you can use chunk partitioning. Nel partizionamento in blocchi ogni thread o attività in un ciclo o una query parallela usa un determinato numero di elementi di origine in un blocco, li elabora e quindi torna a recuperare altri elementi.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. Il partitioner garantisce che tutti gli elementi vengano distribuiti e che non siano presenti duplicati.The partitioner ensures that all elements are distributed and that there are no duplicates. Un blocco può essere di qualsiasi dimensione.A chunk may be any size. Ad esempio, il partitioner illustrato in Procedura: Implementare partizioni dinamiche crea blocchi che contengono un solo elemento.For example, the partitioner that is demonstrated in How to: Implement Dynamic Partitions creates chunks that contain just one element. Purché i blocchi non siano troppo grandi, questo tipo di partizionamento esegue di per sé il bilanciamento del carico perché l'assegnazione di elementi ai thread non è predeterminata.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. Il partitioner, tuttavia, comporta il sovraccarico di sincronizzazione ogni volta che il thread deve ottenere un altro blocco.However, the partitioner does incur the synchronization overhead each time the thread needs to get another chunk. La quantità di operazioni di sincronizzazione eseguite in questi casi è inversamente proporzionale alle dimensioni dei blocchi.The amount of synchronization incurred in these cases is inversely proportional to the size of the chunks.

Il partizionamento per intervalli è in generale più veloce solo quando il tempo di esecuzione del delegato è da breve a medio, l'origine ha un numero elevato di elementi e il lavoro totale di ogni partizione è quasi equivalente.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. Il partizionamento in blocchi è quindi in genere più veloce nella maggior parte dei casi.Chunk partitioning is therefore generally faster in most cases. Nelle origini con un numero limitato di elementi o tempi di esecuzione più lunghi per il delegato le prestazioni del partizionamento in blocchi e per intervalli sono più o meno uguali.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.

I partitioner TPL supportano anche un numero di partizioni dinamico.The TPL partitioners also support a dynamic number of partitions. Ciò significa che possono creare le partizioni immediatamente, ad esempio, quando il ciclo ForEach genera una nuova attività.This means they can create partitions on-the-fly, for example, when the ForEach loop spawns a new task. Questa funzionalità consente al partitioner di essere ridimensionato con il ciclo stesso.This feature enables the partitioner to scale together with the loop itself. I partitioner dinamici eseguono anche implicitamente il bilanciamento del carico.Dynamic partitioners are also inherently load-balancing. Quando si crea un partitioner personalizzato, è necessario supportare il partizionamento dinamico perché sia utilizzabile da un ciclo ForEach.When you create a custom partitioner, you must support dynamic partitioning to be consumable from a ForEach loop.

Configurazione di partitioner di bilanciamento del carico per PLINQConfiguring Load Balancing Partitioners for PLINQ

Alcuni overload del metodo Partitioner.Create consentono di creare un partitioner per una matrice o un'origine IList e di specificare se deve provare a bilanciare il carico di lavoro tra i thread.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. Quando il partitioner è configurato per il bilanciamento del carico, viene usato il partizionamento in blocchi e gli elementi vengono passati a ogni partizione in piccoli blocchi quando vengono richiesti.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. Questo approccio consente di garantire che tutte le partizioni abbiano elementi da elaborare finché l'intero ciclo o query non viene completata.This approach helps ensure that all partitions have elements to process until the entire loop or query is completed. Si può usare un overload aggiuntivo per fornire il partizionamento del bilanciamento del carico di qualsiasi origine IEnumerable.An additional overload can be used to provide load-balancing partitioning of any IEnumerable source.

Per il bilanciamento del carico le partizioni devono in genere richiedere gli elementi al partitioner relativamente spesso.In general, load balancing requires the partitions to request elements relatively frequently from the partitioner. Al contrario, un partitioner che esegue il partizionamento statico può assegnare tutti gli elementi a ogni partitioner contemporaneamente usando il partizionamento per intervalli o in blocchi.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. Ciò comporta un sovraccarico minore rispetto al bilanciamento del carico, ma l'esecuzione potrebbe richiedere più tempo se un thread termina con molte più attività rispetto agli altri.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. Per impostazione predefinita, quando viene passato un elemento IList o una matrice, PLINQ usa sempre il partizionamento per intervalli senza bilanciamento del carico.By default when it is passed an IList or an array, PLINQ always uses range partitioning without load balancing. Per abilitare il bilanciamento del carico per PLINQ, usare il metodo Partitioner.Create, come illustrato nell'esempio seguente.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))

Il modo migliore per stabilire se è opportuno usare il bilanciamento del carico in qualsiasi scenario specificato consiste nello sperimentare e misurare il tempo necessario per il completamento delle operazioni con carichi e configurazioni di computer rappresentativi.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. Il partizionamento statico, ad esempio, potrebbe offrire un considerevole aumento di velocità in un computer con solo pochi core, ma potrebbe comportare rallentamenti nei computer con un numero relativamente elevato di core.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.

Nella tabella seguente sono elencati gli overload disponibili del metodo Create.The following table lists the available overloads of the Create method. Questi partitioner non sono limitati all'uso con PLINQ o Task.These partitioners are not limited to use only with PLINQ or Task. Possono essere usati anche con qualsiasi costrutto parallelo personalizzato.They can also be used with any custom parallel construct.

OverloadOverload Usa il bilanciamento del caricoUses load balancing
Create<TSource>(IEnumerable<TSource>) AlwaysAlways
Create<TSource>(TSource[], Boolean) Quando l'argomento booleano viene specificato come trueWhen the Boolean argument is specified as true
Create<TSource>(IList<TSource>, Boolean) Quando l'argomento booleano viene specificato come trueWhen the Boolean argument is specified as true
Create(Int32, Int32) NeverNever
Create(Int32, Int32, Int32) NeverNever
Create(Int64, Int64) NeverNever
Create(Int64, Int64, Int64) NeverNever

Configurazione di partitioner per intervalli statici per Parallel.ForEachConfiguring Static Range Partitioners for Parallel.ForEach

In un ciclo For il corpo del ciclo viene fornito al metodo come delegato.In a For loop, the body of the loop is provided to the method as a delegate. Il costo della chiamata a tale delegato è più o meno lo stesso di una chiamata a un metodo virtuale.The cost of invoking that delegate is about the same as a virtual method call. In alcuni scenari il corpo di un ciclo parallelo potrebbe essere tanto piccolo che il costo della chiamata al delegato a ogni iterazione del ciclo diventa considerevole.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. In tali situazioni, è possibile usare uno degli overload Create per creare un'interfaccia IEnumerable<T> di partizioni a intervalli sugli elementi di origine.In such situations, you can use one of the Create overloads to create an IEnumerable<T> of range partitions over the source elements. È quindi possibile passare questa raccolta di intervalli a un metodo ForEach il cui corpo è costituito da un normale ciclo for.Then, you can pass this collection of ranges to a ForEach method whose body consists of a regular for loop. Il vantaggio di questo approccio è che il costo della chiamata al delegato viene applicato una sola volta per intervallo, invece che una volta per elemento.The benefit of this approach is that the delegate invocation cost is incurred only once per range, rather than once per element. L'esempio seguente illustra il modello di 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

Ogni thread nel ciclo riceve la propria classe Tuple<T1,T2> che contiene i valori degli indici iniziali e finali dell'intervallo secondario specificato.Every thread in the loop receives its own Tuple<T1,T2> that contains the starting and ending index values in the specified sub-range. Il ciclo for interno usa i valori fromInclusive e toExclusive per eseguire il ciclo direttamente sulla matrice o sull'interfaccia IList.The inner for loop uses the fromInclusive and toExclusive values to loop over the array or the IList directly.

Uno degli overload Create consente di specificare le dimensioni delle partizioni e il numero di partizioni.One of the Create overloads lets you specify the size of the partitions, and the number of partitions. Questo overload può essere usato negli scenari in cui il lavoro per ogni elemento è così ridotto che anche una sola chiamata a un metodo virtuale per ogni elemento ha un impatto notevole sulle prestazioni.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.

Partitioner personalizzatiCustom Partitioners

In alcuni scenari, potrebbe essere utile o anche necessario implementare un partitioner personalizzato.In some scenarios, it might be worthwhile or even required to implement your own partitioner. Si potrebbe, ad esempio, avere una classe di raccolte personalizzata che è possibile partizionare in modo più efficiente di quanto possono fare i partitioner predefiniti, in base alla propria conoscenza della struttura interna della 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. In altri casi, può rivelarsi utile creare partizioni a intervalli di dimensioni variabili a seconda del tempo necessario per elaborare gli elementi in posizioni diverse della raccolta di origine.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.

Per creare un partitioner personalizzato di base, derivare una classe da System.Collections.Concurrent.Partitioner<TSource> ed eseguire l'override dei metodi virtuali, come illustrato nella tabella seguente.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 Questo metodo viene chiamato una volta dal thread principale e restituisce un'interfaccia IList(IEnumerator(TSource)).This method is called once by the main thread and returns an IList(IEnumerator(TSource)). Ogni thread di lavoro nel ciclo o nella query può chiamare GetEnumerator sull'elenco per recuperare un'interfaccia IEnumerator<T> su una partizione distinta.Each worker thread in the loop or query can call GetEnumerator on the list to retrieve a IEnumerator<T> over a distinct partition.
SupportsDynamicPartitions Restituisce true se si implementa GetDynamicPartitions. In caso contrario, false.Return true if you implement GetDynamicPartitions, otherwise, false.
GetDynamicPartitions Se SupportsDynamicPartitions è true, si può chiamare questo metodo invece di GetPartitions.If SupportsDynamicPartitions is true, this method can optionally be called instead of GetPartitions.

Se i risultati devono essere ordinabili o è necessario l'accesso indicizzato agli elementi, derivare da System.Collections.Concurrent.OrderablePartitioner<TSource> ed eseguire l'override dei metodi virtuali, come illustrato nella tabella seguente.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 Questo metodo viene chiamato una volta dal thread principale e restituisce un'interfaccia IList(IEnumerator(TSource)).This method is called once by the main thread and returns an IList(IEnumerator(TSource)). Ogni thread di lavoro nel ciclo o nella query può chiamare GetEnumerator sull'elenco per recuperare un'interfaccia IEnumerator<T> su una partizione distinta.Each worker thread in the loop or query can call GetEnumerator on the list to retrieve a IEnumerator<T> over a distinct partition.
SupportsDynamicPartitions Restituisce true se si implementa GetDynamicPartitions. In caso contrario, false.Return true if you implement GetDynamicPartitions; otherwise, false.
GetDynamicPartitions In genere, si limita a chiamare GetOrderableDynamicPartitions.Typically, this just calls GetOrderableDynamicPartitions.
GetOrderableDynamicPartitions Se SupportsDynamicPartitions è true, si può chiamare questo metodo invece di GetPartitions.If SupportsDynamicPartitions is true, this method can optionally be called instead of GetPartitions.

La tabella seguente fornisce altri dettagli su come i tre tipi di partitioner di bilanciamento del carico implementano la classe OrderablePartitioner<TSource>.The following table provides additional details about how the three kinds of load-balancing partitioners implement the OrderablePartitioner<TSource> class.

Metodo/ProprietàMethod/Property IList/Matrice senza bilanciamento del caricoIList / Array without Load Balancing IList/Matrice con bilanciamento del caricoIList / Array with Load Balancing IEnumerableIEnumerable
GetOrderablePartitions Usa il partizionamento per intervalliUses range partitioning Usa il partizionamento in blocchi ottimizzato per gli elenchi del partitionCount specificatoUses chunk partitioning optimized for Lists for the partitionCount specified Usa il partizionamento in blocchi creando un numero statico di partizioni.Uses chunk partitioning by creating a static number of partitions.
OrderablePartitioner<TSource>.GetOrderableDynamicPartitions Genera un'eccezione non supportataThrows not-supported exception Usa il partizionamento in blocchi ottimizzato per gli elenchi e le partizioni dinamicheUses chunk partitioning optimized for Lists and dynamic partitions Usa il partizionamento in blocchi creando un numero dinamico di partizioni.Uses chunk partitioning by creating a dynamic number of partitions.
KeysOrderedInEachPartition Restituisce true.Returns true Restituisce true.Returns true Restituisce true.Returns true
KeysOrderedAcrossPartitions Restituisce true.Returns true Restituisce false.Returns false Restituisce false.Returns false
KeysNormalized Restituisce true.Returns true Restituisce true.Returns true Restituisce true.Returns true
SupportsDynamicPartitions Restituisce false.Returns false Restituisce true.Returns true Restituisce true.Returns true

Partizioni dinamicheDynamic Partitions

Se si prevede che il partitioner venga usato in un metodo ForEach, è necessario poter restituire un numero dinamico di partizioni.If you intend the partitioner to be used in a ForEach method, you must be able to return a dynamic number of partitions. Questo significa che il partitioner può fornire un enumeratore per una nuova partizione on demand in qualsiasi momento durante l'esecuzione del ciclo.This means that the partitioner can supply an enumerator for a new partition on-demand at any time during loop execution. In sostanza, quando il ciclo aggiunge una nuova attività parallela, richiede una nuova partizione per tale attività.Basically, whenever the loop adds a new parallel task, it requests a new partition for that task. Se è necessario che i dati siano ordinabili, derivare da System.Collections.Concurrent.OrderablePartitioner<TSource> in modo che a ogni elemento in ogni partizione venga assegnato un indice univoco.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.

Per altre informazioni e un esempio, vedere Procedura: Implementare partizioni dinamiche.For more information, and an example, see How to: Implement Dynamic Partitions.

Contratto per i partitionerContract for Partitioners

Quando si implementa un partitioner personalizzato, seguire queste linee guida per assicurare la corretta interazione con PLINQ e ForEach in TPL:When you implement a custom partitioner, follow these guidelines to help ensure correct interaction with PLINQ and ForEach in the TPL:

  • Se GetPartitions viene chiamato con un argomento pari a zero o meno per partitionsCount, generare ArgumentOutOfRangeException.If GetPartitions is called with an argument of zero or less for partitionsCount, throw ArgumentOutOfRangeException. Anche se PLINQ e TPL non passeranno mai un partitionCount uguale a 0, è tuttavia consigliabile proteggersi da questa possibilità.Although PLINQ and TPL will never pass in a partitionCount equal to 0, we nevertheless recommend that you guard against the possibility.

  • GetPartitions e GetOrderablePartitions devono restituire sempre un numero di partizioni pari a partitionsCount.GetPartitions and GetOrderablePartitions should always return partitionsCount number of partitions. Se il partitioner non ha dati sufficienti e non può creare tutte le partizioni richieste, il metodo deve restituire un enumeratore vuoto per ogni partizione rimanente.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. In caso contrario, sia PLINQ che TPL genereranno un'eccezione InvalidOperationException.Otherwise, both PLINQ and TPL will throw an InvalidOperationException.

  • GetPartitions, GetOrderablePartitions, GetDynamicPartitions e GetOrderableDynamicPartitions non devono mai restituire null (Nothing in Visual Basic),GetPartitions, GetOrderablePartitions, GetDynamicPartitions, and GetOrderableDynamicPartitions should never return null (Nothing in Visual Basic). altrimenti PLINQ/TPL genererà un'eccezione InvalidOperationException.If they do, PLINQ / TPL will throw an InvalidOperationException.

  • I metodi che restituiscono partizioni devono sempre restituire partizioni che possano enumerare l'origine dati in modo completo e univoco.Methods that return partitions should always return partitions that can fully and uniquely enumerate the data source. Non devono essere presenti duplicazioni nell'origine dati o elementi ignorati se non specificamente richiesto dalla progettazione del partitioner.There should be no duplication in the data source or skipped items unless specifically required by the design of the partitioner. Se questa regola non viene seguita, l'ordine dell'output potrebbe non risultare corretto.If this rule is not followed, then the output order may be scrambled.

  • I getter booleani seguenti devono sempre restituire in modo accurato i valori seguenti in modo che l'ordine dell'output sia corretto:The following Boolean getters must always accurately return the following values so that the output order is not scrambled:

    • KeysOrderedInEachPartition: ogni partizione restituisce elementi con indici di chiave crescenti.KeysOrderedInEachPartition: Each partition returns elements with increasing key indices.

    • KeysOrderedAcrossPartitions: per tutte le partizioni restituite, gli indici di chiave nella partizione i hanno un valore superiore rispetto a quelli nella partizione 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: tutti gli indici di chiave sono a incremento progressivo costante senza gap, a partire da zero.KeysNormalized: All key indices are monotonically increasing without gaps, starting from zero.

  • Tutti gli indici devono essere univoci.All indices must be unique. Non possono essere presenti indici duplicati.There may not be duplicate indices. Se questa regola non viene seguita, l'ordine dell'output potrebbe non risultare corretto.If this rule is not followed, then the output order may be scrambled.

  • Tutti gli indici devono essere non negativi.All indices must be nonnegative. Se questa regola non viene rispettata, PLINQ/TPL può generare eccezioni.If this rule is not followed, then PLINQ/TPL may throw exceptions.

Vedere ancheSee also