Megosztás a következőn keresztül:


Egyéni particionálók PLINQ-hoz és TPL-hez

Egy adatforráson végzett művelet párhuzamossá alakításához az egyik alapvető lépés a forrás több szakaszra való particionálása , amely egyszerre több szálon keresztül is elérhető. A PLINQ és a Feladat párhuzamos kódtára (TPL) olyan alapértelmezett particionálókat biztosít, amelyek transzparensen működnek párhuzamos lekérdezés vagy ForEach ciklus írásakor. Speciálisabb forgatókönyvek esetén csatlakoztathatja a saját particionálóját.

A particionálás típusai

Az adatforrások particionálásának számos módja van. A leghatékonyabb megközelítésekben több szál is együttműködik az eredeti forrásütemezés feldolgozásában, ahelyett, hogy a forrást fizikailag több részhalmazra különítenék el. Tömbök és egyéb indexelt források, például IList olyan gyűjtemények esetében, ahol a hossz előre ismert, a tartományparticionálás a particionálás legegyszerűbb típusa. Minden szál egyedi kezdő és befejező indexeket kap, hogy a forrástartományát felülírás vagy bármely más szál felülírása nélkül feldolgozhassa. A tartományok particionálásának egyetlen többlettere a tartományok létrehozásának kezdeti munkája; ezután nincs szükség további szinkronizálásra. Ezért jó teljesítményt biztosíthat mindaddig, amíg a számítási feladat egyenlően oszlik el. A tartományparticionálás hátránya, hogy ha az egyik szál korán befejeződik, a többi szál nem tudja befejezni a munkát.

Csatolt listák vagy más olyan gyűjtemények esetében, amelyek hossza nem ismert, használhatja az adattömb particionálását. Az adattömb particionálásában a párhuzamos hurkok vagy lekérdezések minden szála vagy feladata felhasznál néhány forráselemet egy adattömbben, feldolgozza őket, majd visszatér további elemek lekéréséhez. A particionáló biztosítja, hogy az összes elem el legyen osztva, és hogy ne legyenek ismétlődések. Az adattömb bármilyen méretű lehet. A " Dinamikus partíciók implementálása" című témakörben bemutatott particionáló például olyan adattömböket hoz létre, amelyek csak egy elemet tartalmaznak. Mindaddig, amíg a darabok nem túl nagyok, az ilyen típusú particionálás eredendően terheléselosztó, mert az elemek szálakhoz való hozzárendelése nincs előre meghatározva. A particionáló azonban minden alkalommal, amikor a szálnak egy újabb adattömbre van szüksége, szinkronizálási többletterhelést eredményez. Az ilyen esetekben felmerülő szinkronizálás mennyisége fordítottan arányos az adattömbök méretével.

Általánosságban elmondható, hogy a tartományparticionálás csak akkor gyorsabb, ha a delegált végrehajtási ideje kicsi vagy közepes, és a forrás sok elemből áll, és az egyes partíciók teljes munkája nagyjából egyenértékű. Az adattömb particionálása ezért általában gyorsabb a legtöbb esetben. A delegált kis számú elemével vagy hosszabb végrehajtási idejével rendelkező források esetében az adattömb és a tartomány particionálásának teljesítménye körülbelül egyenlő.

A TPL particionálók dinamikus számú partíciót is támogatnak. Ez azt jelenti, hogy akár menet közben is létrehozhatnak partíciókat, például amikor a ForEach hurok új feladatot hoz létre. Ez a funkció lehetővé teszi, hogy a particionáló a hurokkal együtt skálázható legyen. A dinamikus particionálók is eredendően terheléselosztást jelentenek. Egyéni particionáló létrehozásakor támogatnia kell a dinamikus particionálást, hogy egy ForEach ciklusból használható legyen.

Terheléselosztási partíciók konfigurálása PLINQ-hoz

A Partitioner.Create metódus néhány változata lehetővé teszi, hogy particionálót hozzon létre egy tömbhöz vagy IList forráshoz, és megadja, hogy meg kell-e kísérelni a munkaterhelés kiegyensúlyozását a szálak között. Amikor a particionáló terheléselosztásra van konfigurálva, az adattömb particionálást használja, és az elemeket a rendszer kis adattömbökben adja át az egyes partícióknak a kérésnek megfelelően. Ez a megközelítés segít biztosítani, hogy minden partíciónak legyen feldolgozandó eleme, amíg a teljes ciklus vagy lekérdezés be nem fejeződik. További túlterhelési lehetőséggel bármely forrás terheléselosztási particionálását IEnumerable megvalósíthatja.

A terheléselosztás általában megköveteli, hogy a partíciók viszonylag gyakran kérjenek elemeket a particionálótól. Ezzel szemben a statikus particionálást használó particionálók egyszerre rendelhetik hozzá az elemeket az egyes particionálókhoz tartomány vagy adattömb particionálásával. Ez kevesebb többletterhelést igényel, mint a terheléselosztás, de hosszabb időt is igénybe vehet, ha egy szál a többinél lényegesen több munkával végződik. Az IList- vagy tömbátadáskor a PLINQ alapértelmezés szerint terheléselosztás nélkül használ tartományparticionálást. A PLINQ terheléselosztásának engedélyezéséhez használja a Partitioner.Create metódust az alábbi példában látható módon.

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

A legjobb módszer annak meghatározására, hogy a terheléselosztást egy adott forgatókönyvben érdemes-e használni, ha kísérletezik és méri, hogy mennyi ideig tart a műveletek végrehajtása reprezentatív terhelések és számítógép-konfigurációk esetén. A statikus particionálás például jelentős felgyorsulást okozhat egy többmagos számítógépen, amely csak néhány maggal rendelkezik, de a viszonylag sok maggal rendelkező számítógépeken lassulást eredményezhet.

Az alábbi táblázat a metódus rendelkezésre álló túlterheléseit sorolja fel Create . Ezek a particionálók nincsenek korlátozva arra, hogy csak a PLINQ-val vagy Task használhatók legyenek. Bármely egyéni párhuzamos szerkezettel is használhatók.

Túlterhelés Terheléselosztást használ
Create<TSource>(IEnumerable<TSource>) Mindig
Create<TSource>(TSource[], Boolean) Amikor a logikai argumentum értékét igazra állítják
Create<TSource>(IList<TSource>, Boolean) Amikor a logikai argumentum értékét igazra állítják
Create(Int32, Int32) Soha
Create(Int32, Int32, Int32) Soha
Create(Int64, Int64) Soha
Create(Int64, Int64, Int64) Soha

Statikus tartomány particionálóinak konfigurálása a Parallel.ForEachhoz

For Egy ciklusban a ciklus törzse delegáltként kerül átadásra a metódusnak. A meghatalmazott meghívásának költsége nagyjából megegyezik egy virtuális metódushívással. Bizonyos esetekben a párhuzamos ciklus törzse elég kicsi lehet ahhoz, hogy a delegált hívás költsége az egyes ciklusok iterációinál jelentős legyen. Ilyen helyzetekben az egyik Create túlterhelést használva IEnumerable<T> tartománypartíciókat hozhat létre a forráselemek fölött. Ezután átadhatja ezt a tartománygyűjteményt egy ForEach olyan metódusnak, amelynek törzse normál for hurokból áll. Ennek a megközelítésnek az az előnye, hogy a delegálási hívás költsége tartományonként csak egyszer merül fel, nem pedig elemenként egyszer. Az alábbi példa az alapmintát mutatja be.

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

A hurok minden szála megkapja a sajátját Tuple<T1,T2> , amely a megadott altartomány kezdő és záró indexértékeit tartalmazza. A belső for hurok a fromInclusive és toExclusive értékek használatával iterál végig a tömbön vagy közvetlenül a IList-en.

Az egyik Create túlterhelés lehetővé teszi a partíciók méretének és a partíciók számának megadását. Ez a túlterhelés olyan helyzetekben használható, ahol az elemenkénti munka olyan alacsony, hogy elemenként még egy virtuális metódus hívása is jelentős hatással van a teljesítményre.

Egyedi particionálók

Bizonyos esetekben érdemes vagy akár szükséges is a saját particionáló implementálása. Előfordulhat például, hogy rendelkezik egy egyéni gyűjteményosztálysal, amelyet hatékonyabban particionálhat, mint az alapértelmezett particionálók, az osztály belső szerkezetének ismerete alapján. Vagy érdemes lehet különböző méretű tartománypartíciókat létrehozni annak ismerete alapján, hogy mennyi ideig fog tartani az elemek feldolgozása a forrásgyűjtemény különböző helyeinél.

Alapszintű egyéni particionáló létrehozásához származtasson egy osztályt a System.Collections.Concurrent.Partitioner<TSource> osztályból, és a virtuális metódusokat írja felül a következő táblázatban leírtak szerint.

Metódus Leírás
GetPartitions Ezt a metódust a főszál egyszer hívja meg, és egy IList(IEnumerator(TSource)) értéket ad vissza. A ciklus vagy lekérdezés minden feldolgozószála meghívhatja GetEnumerator a listán, hogy egy különálló partícióhoz lekérjen egy IEnumerator<T>-t.
SupportsDynamicPartitions Adja vissza true, amennyiben implementálja GetDynamicPartitions, egyébként false.
GetDynamicPartitions Ha a SupportsDynamicPartitionstrue, ez a metódus opcionálisan meghívható a GetPartitions helyett.

Ha az eredményeknek rendezhetőnek kell lenniük, vagy indexelt hozzáférést szeretne az elemekhez, akkor származtassa System.Collections.Concurrent.OrderablePartitioner<TSource>-ből és bírálja felül a virtuális metódusokat az alábbi táblázatban ismertetett módon.

Metódus Leírás
GetPartitions Ezt a metódust a főszál egyszer hívja meg, és visszaad egy IList(IEnumerator(TSource)). A ciklus vagy lekérdezés minden feldolgozószála meghívhatja GetEnumerator a listán, hogy egy különálló partícióhoz lekérjen egy IEnumerator<T>-t.
SupportsDynamicPartitions Adja vissza true, ha implementálja GetDynamicPartitions; ellenkező esetben „hamis”.
GetDynamicPartitions Általában ez csak a GetOrderableDynamicPartitions hívja meg.
GetOrderableDynamicPartitions Ha a SupportsDynamicPartitionstrue, ez a metódus opcionálisan meghívható a GetPartitions helyett.

Az alábbi táblázat további részleteket tartalmaz arról, hogy a háromféle terheléselosztási particionáló hogyan valósítja meg az osztályt OrderablePartitioner<TSource> .

Metódus/tulajdonság IList/ Tömb terheléselosztás nélkül IList/Tömb terheléselosztással IEnumerable
GetOrderablePartitions Tartomány particionálását használja A listákhoz optimalizált darabokra osztást használ a megadott partíciós szám alapján. Adattömb particionálást használ statikus számú partíció létrehozásával.
OrderablePartitioner<TSource>.GetOrderableDynamicPartitions Nem támogatott kivételt eredményez Listákhoz és dinamikus partíciókhoz optimalizált adattömbpartíciót használ Adattömb particionálást használ dinamikus számú partíció létrehozásával.
KeysOrderedInEachPartition Visszatér true Visszatér true Visszatér true
KeysOrderedAcrossPartitions Visszatér true Visszatér false Visszatér false
KeysNormalized Visszatér true Visszatér true Visszatér true
SupportsDynamicPartitions Visszatér false Visszatér true Visszatér true

Dinamikus partíciók

Ha a particionálót egy ForEach metódusban kívánja használni, dinamikus számú partíciót kell visszaadnia. Ez azt jelenti, hogy a particionáló a ciklus végrehajtása során bármikor képes enumerátort biztosítani egy igény szerinti új partícióhoz. Alapvetően amikor a hurok új párhuzamos feladatot ad hozzá, új partíciót kér ehhez a feladathoz. Ha azt szeretné, hogy az adatok rendezettek legyenek, akkor abból System.Collections.Concurrent.OrderablePartitioner<TSource> származtatva az egyes partíciók egyes elemei egyedi indexet kapnak.

További információt és egy példát a Dinamikus partíciók implementálása című témakörben talál.

Particionálók szerződése

Egyéni particionáló implementálásakor kövesse az alábbi irányelveket a PLINQ és ForEach a TPL megfelelő interakciójának biztosításához:

  • Ha GetPartitions nulla vagy annál kisebb partitionsCount argumentummal van meghívva, dobjuk a ArgumentOutOfRangeException kivételt. Bár a PLINQ és a TPL soha nem ad át egy 0-val egyenlő partitionCount-t, mégis javasoljuk, hogy tanácsos védekezni az ellen a lehetőség ellen.

  • GetPartitions és GetOrderablePartitions mindig partitionsCount partíciószámot kell visszaadjon. Ha a particionáló elfogy az adatokból, és nem tud annyi partíciót létrehozni, amennyit kért, akkor a metódusnak üres enumerátort kell visszaadnia az összes többi partícióhoz. Ellenkező esetben a PLINQ és a TPL is egy InvalidOperationException.

  • GetPartitions, GetOrderablePartitions, GetDynamicPartitionsés GetOrderableDynamicPartitions soha nem térhet vissza null (Nothing a Visual Basicben). Ha igen, PLINQ / TPL fog dobni egy InvalidOperationException.

  • A partíciókat visszaadó metódusoknak mindig olyan partíciókat kell visszaadni, amelyek teljes mértékben és egyedileg számba tudják sorolni az adatforrást. Az adatforrásban és a kihagyott elemekben nem lehet duplikáció, kivéve, ha a particionáló tervezése kifejezetten megköveteli. Ha ezt a szabályt nem követi, előfordulhat, hogy a kimeneti sorrend össze van firkálva.

  • A következő Boolean lekérdezéseknek mindig pontosan kell visszaadni a következő értékeket, hogy a kimeneti sorrend ne legyen összekuszálódott.

    • KeysOrderedInEachPartition: Minden partíció növekvő kulcsindexekkel rendelkező elemeket ad vissza.

    • KeysOrderedAcrossPartitions: Az összes visszaadott partíció esetében az i partíció kulcsindexei magasabbak, mint az i-1 partíció kulcsindexei.

    • KeysNormalized: Az összes fő index monoton módon növekszik hézagok nélkül, nullától kezdve.

  • Minden indexnek egyedinek kell lennie. Előfordulhat, hogy nem ismétlődnek az indexek. Ha ezt a szabályt nem követi, előfordulhat, hogy a kimeneti sorrend össze van firkálva.

  • Az összes indexnek nemnegatívnak kell lennie. Ha ezt a szabályt nem követi, akkor a PLINQ/TPL kivételeket okozhat.

Lásd még