Poznámka
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Pokud chcete paralelizovat operaci ve zdroji dat, jedním z základních kroků je rozdělení zdroje do několika oddílů, ke kterým může přistupovat souběžně více vláken. PLINQ a knihovna úloh Task Parallel Library (TPL) poskytují výchozí partitionery, které transparentně fungují při psaní paralelního dotazu nebo smyčky ForEach. Pro pokročilejší scénáře můžete připojit vlastního dělitele.
Druhy dělení
Existuje mnoho způsobů, jak rozdělit zdroj dat. V nejúčinnějších přístupech spolupracuje více vláken na zpracování původní zdrojové sekvence namísto fyzického oddělení zdroje do několika dílčích sekvencí. Pro pole a další indexované zdroje, jako jsou kolekce, kde je délka známa předem, je IList nejjednodušším druhem rozdělení. Každé vlákno dostane jedinečné počáteční a koncové indexy, aby mohlo zpracovat svůj rozsah zdroje, aniž by přepsalo jiná vlákna nebo bylo jimi přepsáno. Jedinou režií spojenou s dělením rozsahu je počáteční práce vytváření rozsahů; Po této operaci není nutná žádná další synchronizace. Proto může poskytovat dobrý výkon, pokud je úloha rovnoměrně rozdělena. Nevýhodou dělení rozsahu je, že pokud se jedno vlákno dokončí brzy, nemůže pomoct ostatním vláknům dokončit svou práci.
U propojených seznamů nebo jiných kolekcí, jejichž délka není známá, můžete použít dělení bloků dat. Při dělení bloků dat využívá každé vlákno nebo úkol v paralelní smyčce nebo dotazu určitý počet zdrojových prvků v jednom bloku, zpracuje je a pak se vrátí k načtení dalších prvků. Rozdělovač zajišťuje distribuci všech prvků a že neexistují žádné duplicity. Blok dat může mít libovolnou velikost. Například partitioner, který je ukázán v Jak na to: Implementace dynamických oddílů, vytváří bloky, které obsahují pouze jeden prvek. Pokud části nejsou příliš velké, tento druh dělení zajišťuje vyrovnávání zatížení, protože přiřazení prvků k vláknům není předem určeno. Když však partitioner pokaždé načte další blok, dochází k režii synchronizace. Množství synchronizace vzniklé v těchto případech je inverzní úměrné velikosti bloků dat.
Obecně platí, že rozsahové dělení je rychlejší jen tehdy, když doba provádění delegáta je malá až střední, zdroj má velký počet prvků a celkový objem práce každé části je přibližně ekvivalentní. Dělení bloků dat je proto ve většině případů obecně rychlejší. U zdrojů s malým počtem prvků nebo delší dobou provádění delegáta je výkon bloků dat a dělení rozsahu přibližně stejný.
Nástroje pro dělení TPL také podporují dynamický počet oddílů. To znamená, že mohou vytvářet oddíly dynamicky, například když smyčka ForEach vytvoří novou úlohu. Tato funkce umožňuje rozdělovači škálovat současně se samotnou smyčkou. Dynamické děliče jsou také ze své podstaty vyvažující zatížení. Při vytváření vlastního partitioneru musíte podporovat dynamické dělení, aby bylo možné ho použít ze smyčky ForEach .
Konfigurace particionérů vyrovnávání zatížení pro PLINQ
Některá přetížení metody Partitioner.Create umožňují vytvořit partitioner pro pole nebo zdroj IList a určit, zda by se měla pokusit rovnoměrně rozložit zátěž mezi vlákna. Když je partitioner nakonfigurován na vyrovnávání zatížení, používá se chunkové dělení a prvky se předávají do každého oddílu v malých balíčcích podle potřeby. Tento přístup pomáhá zajistit, aby všechny partice měly prvky ke zpracování až do dokončení celé smyčky nebo dotazu. Další přetížení lze použít k zajištění dělení pro vyrovnávání zatížení libovolného IEnumerable zdroje.
Vyrovnávání zatížení obecně vyžaduje, aby oddíly relativně často požadovaly prvky od dělícího modulu. Naproti tomu rozdělovací mechanismus, který provádí statické dělení, může přiřadit prvky každému oddílu najednou buď pomocí rozsahového, nebo blokového dělení. To vyžaduje menší režii než vyrovnávání zatížení, ale může trvat déle, než se vykoná, pokud jedno vlákno skončí s výrazně více práce než ostatní. Ve výchozím nastavení při předání IList nebo pole plINQ vždy používá dělení rozsahu bez vyrovnávání zatížení. Pokud chcete povolit vyrovnávání zatížení pro PLINQ, použijte metodu Partitioner.Create
, jak je znázorněno v následujícím příkladu.
// 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))
Nejlepší způsob, jak určit, jestli použít vyrovnávání zatížení v jakémkoli daném scénáři, je experimentovat a měřit, jak dlouho trvá dokončení operací v rámci reprezentativních zatížení a konfigurací počítače. Statické dělení může například výrazně zrychlit na počítači s více jádry, který má pouze několik jader, ale může způsobit zpomalení na počítačích s relativně velkým počtem jader.
Následující tabulka uvádí dostupné přetížení Create metody. Tyto dělicí nástroje nejsou omezeny pouze na použití s PLINQ nebo Task. Lze je také použít s libovolným vlastním paralelním konstruktorem.
Přetížení | Používá vyrovnávání zatížení. |
---|---|
Create<TSource>(IEnumerable<TSource>) | Vždy |
Create<TSource>(TSource[], Boolean) | Pokud je logický argument zadán jako true |
Create<TSource>(IList<TSource>, Boolean) | Pokud je logický argument zadán jako true |
Create(Int32, Int32) | Nikdy |
Create(Int32, Int32, Int32) | Nikdy |
Create(Int64, Int64) | Nikdy |
Create(Int64, Int64, Int64) | Nikdy |
Nastavení statických rozdělovačů rozsahu pro Parallel.ForEach
For Ve smyčce je tělo smyčky předáváno metodě jako delegát. Náklady na vyvolání delegáta jsou přibližně stejné jako volání virtuální metody. V některých scénářích může být tělo paralelního cyklu dostatečně malé, aby náklady na vyvolání delegáta v každé iteraci mohly být významné. V takových situacích můžete použít jedno z Create přetížení k vytvoření IEnumerable<T> rozsahových oddílů pro zdrojové prvky. Tuto kolekci oblastí ForEach pak můžete předat metodě, jejíž tělo se skládá z pravidelné for
smyčky. Výhodou tohoto přístupu je, že náklady na vyvolání delegáta se účtují pouze jednou za rozsah, nikoli jednou za prvek. Následující příklad ukazuje základní vzor.
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
Každé vlákno ve smyčce obdrží vlastní Tuple<T1,T2>, které obsahuje počáteční a koncové hodnoty indexu v zadaném podrozsahu. Vnitřní for
smyčka používá hodnoty fromInclusive
a toExclusive
k procházení pole nebo přímo IList.
Jedno z Create přetížení umožňuje určit velikost oddílů a počet oddílů. Toto přetížení lze použít ve scénářích, kdy je práce na prvek tak nízká, že dokonce i jedno volání virtuální metody na prvek má výrazný dopad na výkon.
Vlastní rozdělovače
V některých scénářích může být vhodné nebo dokonce vyžadovat implementaci vlastního partitioneru. Můžete mít například vlastní třídu kolekce, kterou můžete efektivněji rozdělit, než mohou výchozí dělicí nástroje na základě vašich znalostí o interní struktuře třídy. Nebo můžete chtít vytvořit oddíly rozsahu různých velikostí na základě znalostí o tom, jak dlouho bude trvat zpracování prvků v různých umístěních ve zdrojové kolekci.
Chcete-li vytvořit základní vlastní dělicí nástroj, odvoďte třídu z System.Collections.Concurrent.Partitioner<TSource> a přepište virutální metody, jak je popsáno v následující tabulce.
Metoda | Popis |
---|---|
GetPartitions | Tuto metodu hlavní vlákno volá jednou a vrátí IList(IEnumerator(TSource)). Každé pracovní vlákno ve smyčce nebo dotazu může volat GetEnumerator na seznamu, aby načetlo IEnumerator<T> z odlišného oddílu. |
SupportsDynamicPartitions | Vrátí true , pokud implementujete GetDynamicPartitions, jinak false . |
GetDynamicPartitions | Pokud SupportsDynamicPartitions je true , tato metoda může být volitelně volána místo GetPartitions. |
Pokud musí být výsledky seřazené nebo vyžadujete indexovaný přístup k prvkům, odvozujte z System.Collections.Concurrent.OrderablePartitioner<TSource> a přepište jeho virtuální metody, jak je popsáno v následující tabulce.
Metoda | Popis |
---|---|
GetPartitions | Tato metoda je volána jednou hlavním vláknem a vrátí IList(IEnumerator(TSource)) . Každé pracovní vlákno ve smyčce nebo dotazu může volat GetEnumerator na seznamu, aby načetlo IEnumerator<T> z odlišného oddílu. |
SupportsDynamicPartitions | Vrátí hodnotu true , pokud implementujete GetDynamicPartitions, jinak nepravda. |
GetDynamicPartitions | Obvykle to jen volá GetOrderableDynamicPartitions. |
GetOrderableDynamicPartitions | Pokud SupportsDynamicPartitions je true , tato metoda může být volitelně volána místo GetPartitions. |
Následující tabulka obsahuje další podrobnosti o tom, jak tři druhy rozdělovačů vyrovnávání zatížení implementují OrderablePartitioner<TSource> třídu.
Metoda/vlastnost | IList / Pole bez vyrovnávání zatížení | IList / Array s vyrovnáváním zatížení | IEnumerable |
---|---|---|---|
GetOrderablePartitions | Používá dělení rozsahu. | Používá dělení bloků dat optimalizované pro seznamy pro zadaný počet oddílů. | Používá dělení bloků dat vytvořením statického počtu oddílů. |
OrderablePartitioner<TSource>.GetOrderableDynamicPartitions | Vyvolá výjimku, která není podporována. | Používá dělení bloků dat optimalizované pro seznamy a dynamické oddíly. | Používá dělení bloků dat vytvořením dynamického počtu oddílů. |
KeysOrderedInEachPartition | Návrat true |
Návrat true |
Návrat true |
KeysOrderedAcrossPartitions | Návrat true |
Návrat false |
Návrat false |
KeysNormalized | Návrat true |
Návrat true |
Návrat true |
SupportsDynamicPartitions | Návrat false |
Návrat true |
Návrat true |
Dynamické oddíly
Pokud chcete použít partitioner v ForEach metodě, musíte být schopni vrátit dynamický počet oddílů. To znamená, že partitioner může kdykoli během provádění smyčky poskytnout výčet nového oddílu na vyžádání. V podstatě, kdykoli smyčka přidá novou paralelní úlohu, požádá o nový oddíl pro daný úkol. Pokud potřebujete, aby byla data uspořádaná, odvozujte je z System.Collections.Concurrent.OrderablePartitioner<TSource> toho, aby každá položka v každém oddílu byla přiřazena jedinečnému indexu.
Další informace a příklad najdete v tématu Jak na to: Implementace dynamických oddílů.
Kontrakt pro dělitele
Při implementaci vlastního partitioneru postupujte podle těchto pokynů, abyste zajistili správnou interakci s PLINQ a ForEach v TPL:
Pokud je GetPartitions volána s argumentem nula nebo méně pro
partitionsCount
, vyvolejte ArgumentOutOfRangeException. I když PLINQ a TPL nikdy neposkytnou hodnotu rovnoupartitionCount
0, přesto doporučujeme, abyste se chránili proti této možnosti.GetPartitions a GetOrderablePartitions by vždy měly vracet počet oddílů
partitionsCount
. Pokud partitioner vyčerpá data a nemůže vytvořit tolik oddílů, kolik požadujete, pak by metoda měla vrátit prázdný výčet pro každý z zbývajících oddílů. V opačném případě PLINQ i TPL vyvolají výjimku InvalidOperationException.GetPartitions, GetOrderablePartitions, GetDynamicPartitionsa GetOrderableDynamicPartitions nikdy by se neměly vracet
null
(Nothing
v jazyce Visual Basic). Pokud ano, PLINQ / TPL vyvolá InvalidOperationException.Metody vracející oddíly by vždy měly vracet oddíly, které mohou plně a jedinečně vyčíslit zdroj dat. Ve zdroji dat by se nemělo duplikovat ani přeskakovat položky, pokud to návrh oddílu výslovně nevyžaduje. Pokud toto pravidlo není dodrženo, může být výstupní pořadí zpřeházeno.
Následující logické gettery musí vždy přesně vracet následující hodnoty, aby výstupní pořadí nebylo zamícháno:
KeysOrderedInEachPartition
: Každý oddíl vrací prvky se zvýšením klíčových indexů.KeysOrderedAcrossPartitions
: Pro všechny vrácené oddíly jsou klíčové indexy v oddílu i vyšší než klíčové indexy v oddílu i-1.KeysNormalized
: Všechny klíčové indexy se monotonicky zvětšují bez mezer, počínaje nulou.
Všechny indexy musí být jedinečné. Nemusí existovat duplicitní indexy. Pokud toto pravidlo není dodrženo, může být výstupní pořadí zpřeházeno.
Všechny indexy musí být nenegativní. Pokud toto pravidlo není dodrženo, může plINQ/TPL vyvolat výjimky.