Úvod do PLINQ
Paralelní LINQ (PLINQ) je paralelní implementace vzoru LINQ (Language-Integrated Query). PLINQ implementuje úplnou sadu standardních operátorů dotazu LINQ jako rozšiřující metody oboru System.Linq názvů a má další operátory pro paralelní operace. PLINQ kombinuje jednoduchost a čitelnost syntaxe LINQ s výkonem paralelního programování.
Tip
Pokud linQ neznáte, nabízí jednotný model pro dotazování na jakýkoli výčtový zdroj dat bezpečným způsobem. LINQ to Objects je název dotazů LINQ, které se spouštějí s kolekcemi v paměti, jako List<T> jsou a pole. Tento článek předpokládá, že máte základní znalosti LINQ. Další informace najdete v tématu Jazykově integrovaný dotaz (LINQ).
Co je paralelní dotaz?
Dotaz PLINQ se mnoha způsoby podobá ne parallel linQ to Objects dotazu. Dotazy PLINQ, stejně jako sekvenční dotazy LINQ, pracují s jakýmkoliv zdrojem dat v paměti IEnumerable nebo IEnumerable<T> zdroji dat a mají odložené spuštění, což znamená, že se nespouštějí, dokud se dotaz nevyčte. Hlavním rozdílem je, že PLINQ se pokouší plně využít všechny procesory v systému. Provede to rozdělením zdroje dat do segmentů a následným spuštěním dotazu na jednotlivé segmenty v samostatných pracovních vláknech paralelně na více procesorech. V mnoha případech paralelní spouštění znamená, že dotaz běží výrazně rychleji.
Prostřednictvím paralelního spouštění může PLINQ dosáhnout výrazných vylepšení výkonu oproti staršímu kódu pro určité druhy dotazů, často jen přidáním AsParallel operace dotazu do zdroje dat. Paralelismus však může představovat své vlastní složitosti, a ne všechny operace dotazů běží rychleji v PLINQ. Paralelizace ve skutečnosti zpomaluje určité dotazy. Proto byste měli rozumět tomu, jak problémy, jako je řazení, ovlivňují paralelní dotazy. Další informace naleznete v tématu Principy zrychlení v PLINQ.
Poznámka:
Tato dokumentace používá výrazy lambda k definování delegátů v PLINQ. Pokud výrazy lambda v jazyce C# nebo Visual Basic neznáte, přečtěte si téma Výrazy lambda v PLINQ a TPL.
Zbývající část tohoto článku poskytuje přehled hlavních tříd PLINQ a popisuje, jak vytvářet dotazy PLINQ. Každá část obsahuje odkazy na podrobnější informace a příklady kódu.
ParallelEnumerable – třída
Třída System.Linq.ParallelEnumerable zveřejňuje téměř všechny funkce PLINQ. Zkompilují se do System.Core.dll sestavení a ostatní typy System.Linq oborů názvů. Výchozí projekty C# a Visual Basic v sadě Visual Studio odkazují na sestavení a import oboru názvů.
ParallelEnumerable zahrnuje implementace všech standardních operátorů dotazu, které LINQ to Objects podporuje, i když se nepokouší paralelizovat každý z nich. Pokud nemáte zkušenosti s JAZYKEM LINQ, přečtěte si téma Úvod do jazyka LINQ (C#) a Úvod do jazyka LINQ (Visual Basic).
Kromě standardních operátorů ParallelEnumerable dotazu třída obsahuje sadu metod, které umožňují chování specifické pro paralelní spouštění. Tyto metody specifické pro PLINQ jsou uvedeny v následující tabulce.
ParallelEnumerable – operátor | Popis |
---|---|
AsParallel | Vstupní bod pro PLINQ. Určuje, že zbytek dotazu by měl být paralelizován, pokud je to možné. |
AsSequential | Určuje, že zbytek dotazu by se měl spouštět postupně jako ne parallel LINQ dotaz. |
AsOrdered | Určuje, že funkce PLINQ by měla zachovat pořadí zdrojové sekvence pro zbytek dotazu nebo dokud se pořadí nezmění, například pomocí klauzule orderby (Order By v jazyce Visual Basic). |
AsUnordered | Určuje, že funkce PLINQ pro zbytek dotazu není nutná k zachování pořadí zdrojové sekvence. |
WithCancellation | Určuje, že PLINQ by měl pravidelně monitorovat stav poskytnutého tokenu zrušení a zrušit provádění, pokud je požadován. |
WithDegreeOfParallelism | Určuje maximální počet procesorů, které má PLINQ použít k paralelizaci dotazu. |
WithMergeOptions | Poskytuje nápovědu k tomu, jak by měl PLINQ (pokud je to možné), sloučit paralelní výsledky zpět do jedné sekvence na spotřebovacím vlákně. |
WithExecutionMode | Určuje, jestli má PLINQ paralelizovat dotaz, i když by se výchozí chování mělo spouštět postupně. |
ForAll | Metoda vícevláknového výčtu, která na rozdíl od iterace nad výsledky dotazu umožňuje zpracovávat výsledky paralelně bez toho, aby se nejprve sloučily zpět do vlákna příjemce. |
Aggregate Přetížení | Přetížení, které je jedinečné pro PLINQ a umožňuje průběžnou agregaci přes oddíly místních vláken a konečnou agregační funkci pro kombinování výsledků všech oddílů. |
Model výslovného souhlasu
Při psaní dotazu se přihlaste k PLINQ vyvoláním ParallelEnumerable.AsParallel metody rozšíření ve zdroji dat, jak je znázorněno v následujícím příkladu.
var source = Enumerable.Range(1, 10000);
// Opt in to PLINQ with AsParallel.
var evenNums = from num in source.AsParallel()
where num % 2 == 0
select num;
Console.WriteLine("{0} even numbers out of {1} total",
evenNums.Count(), source.Count());
// The example displays the following output:
// 5000 even numbers out of 10000 total
Dim source = Enumerable.Range(1, 10000)
' Opt in to PLINQ with AsParallel
Dim evenNums = From num In source.AsParallel()
Where num Mod 2 = 0
Select num
Console.WriteLine("{0} even numbers out of {1} total",
evenNums.Count(), source.Count())
' The example displays the following output:
' 5000 even numbers out of 10000 total
Rozšiřující AsParallel metoda sváže následující operátory dotazu, v tomto případě where
a select
, na System.Linq.ParallelEnumerable implementace.
Režimy spouštění
Ve výchozím nastavení je PLINQ konzervativní. V době běhu analyzuje infrastruktura PLINQ celkovou strukturu dotazu. Pokud je pravděpodobné, že dotaz přinese zrychlení paralelizací, PLINQ rozdělí zdrojová sekvence do úloh, které je možné spustit souběžně. Pokud není bezpečné paralelizovat dotaz, PLINQ dotaz spustí postupně. Pokud má PLINQ volbu mezi potenciálně drahým paralelním nebo levným sekvenčním algoritmem, ve výchozím nastavení zvolí sekvenční algoritmus. Metodu WithExecutionMode a výčet můžete použít k pokynu System.Linq.ParallelExecutionMode PLINQ k výběru paralelního algoritmu. To je užitečné, když víte, že testováním a měřením se konkrétní dotaz spouští paralelně rychleji. Další informace naleznete v tématu Postupy: Určení režimu spouštění v PLINQ.
Stupeň paralelismu
Ve výchozím nastavení používá PLINQ všechny procesory na hostitelském počítači. PlINQ můžete instruovat, aby pomocí WithDegreeOfParallelism metody nepoužít více než zadaný počet procesorů. To je užitečné, když chcete zajistit, aby ostatní procesy spuštěné v počítači obdržely určitou dobu procesoru. Následující fragment kódu omezuje dotaz na využití maximálně dvou procesorů.
var query = from item in source.AsParallel().WithDegreeOfParallelism(2)
where Compute(item) > 42
select item;
Dim query = From item In source.AsParallel().WithDegreeOfParallelism(2)
Where Compute(item) > 42
Select item
V případech, kdy dotaz provádí značné množství nevýpočtu vázané práce, jako jsou vstupně-výstupní operace souboru, může být užitečné určit stupeň paralelismu větší než počet jader na počítači.
Seřazené a neuspořádané paralelní dotazy
V některých dotazech musí operátor dotazu vytvořit výsledky, které zachová pořadí zdrojové sekvence. PLINQ poskytuje AsOrdered operátor pro tento účel. AsOrdered se liší od AsSequential. Sekvence AsOrdered se stále zpracovává paralelně, ale výsledky se ukládají do vyrovnávací paměti a seřadí. Vzhledem k tomu, že zachování pořadí obvykle zahrnuje další práci, AsOrdered může být sekvence zpracována pomaleji než výchozí AsUnordered sekvence. Určuje, jestli je konkrétní seřazená paralelní operace rychlejší než sekvenční verze operace, závisí na mnoha faktorech.
Následující příklad kódu ukazuje, jak se přihlásit k zachování.
var evenNums =
from num in numbers.AsParallel().AsOrdered()
where num % 2 == 0
select num;
Dim evenNums = From num In numbers.AsParallel().AsOrdered()
Where num Mod 2 = 0
Select num
Další informace naleznete v tématu Zachování objednávek v PLINQ.
Paralelní versus sekvenční dotazy
Některé operace vyžadují, aby se zdrojová data doručila postupně. Operátory ParallelEnumerable dotazu se po vyžádání automaticky vrátí do sekvenčního režimu. Pro uživatelem definované operátory dotazů a delegáty uživatelů, kteří vyžadují postupné spuštění, PLINQ poskytuje metodu AsSequential . Při použití AsSequentialse všechny následné operátory v dotazu spustí postupně, dokud AsParallel se znovu nevolají. Další informace naleznete v tématu Postupy: Kombinování paralelních a sekvenčních dotazů LINQ.
Možnosti sloučení výsledků dotazu
Když se dotaz PLINQ spustí paralelně, musí být jeho výsledky z každého pracovního vlákna sloučeny zpět do hlavního vlákna pro použití smyčkou foreach
(For Each
v jazyce Visual Basic) nebo vložením do seznamu nebo pole. V některých případech může být užitečné určit určitý druh operace sloučení, například začít vytvářet výsledky rychleji. Pro tento účel PLINQ podporuje metodu WithMergeOptionsParallelMergeOptions a výčet. Další informace naleznete v tématu Možnosti sloučení v PLINQ.
Operátor ForAll
V sekvenčních dotazech LINQ se provádění odloží, dokud se dotaz nezačte vyčíslit ve foreach
smyčce (For Each
v jazyce Visual Basic), nebo vyvoláním metody, jako ToList je , ToArray nebo ToDictionary. V PLINQ můžete také použít foreach
ke spuštění dotazu a iteraci výsledků. Sám se foreach
však nespustí paralelně, a proto vyžaduje, aby výstup ze všech paralelních úloh byl sloučen zpět do vlákna, na kterém je smyčka spuštěna. V PLINQ můžete použít foreach
, když musíte zachovat konečné pořadí výsledků dotazu, a také při každém zpracování výsledků sériovým způsobem, například při volání Console.WriteLine
pro každý prvek. Rychlejší provádění dotazů v případě, že není vyžadováno zachování pořadí a kdy se zpracování výsledků může paralelizovat, použijte ForAll metodu ke spuštění plINQ dotazu. ForAll neprovede tento poslední krok sloučení. Následující příklad kódu ukazuje, jak použít metodu ForAll . System.Collections.Concurrent.ConcurrentBag<T> se používá zde, protože je optimalizovaný pro více vláken, které se přidávají souběžně, aniž byste se pokusili odebrat všechny položky.
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)));
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)))
Následující obrázek znázorňuje rozdíl mezi foreach
prováděním dotazů a ForAll s ohledem na jejich provádění.
Zrušení
PLINQ je integrovaný s typy zrušení v .NET. (Další informace najdete v tématu Zrušení ve spravovaných vláknech.) Proto na rozdíl od sekvenčních dotazů LINQ to Objects je možné zrušit dotazy PLINQ. Pokud chcete vytvořit stornoovatelný dotaz PLINQ, použijte WithCancellation operátor dotazu a jako argument zadejte CancellationToken instanci. IsCancellationRequested Pokud je vlastnost tokenu nastavena na true, PLINQ si všimne, zastaví zpracování ve všech vláknech a vyvolá OperationCanceledException.
Je možné, že dotaz PLINQ může i nadále zpracovávat některé prvky po nastavení tokenu zrušení.
Kvůli větší odezvě můžete také reagovat na žádosti o zrušení v dlouhotrvajících delegátech uživatelů. Další informace najdete v tématu Postupy: Zrušení dotazu PLINQ.
Výjimky
Když se spustí dotaz PLINQ, může být současně vyvolána více výjimek z různých vláken. Kód pro zpracování výjimky může být také v jiném vlákně než kód, který výjimku vyvolal. PLINQ používá AggregateException typ k zapouzdření všech výjimek, které byly vyvolány dotazem, a zařaďte tyto výjimky zpět do volajícího vlákna. Ve volajícím vlákně je vyžadován pouze jeden blok try-catch. Můžete však iterovat všechny výjimky, které jsou zapouzdřeny do AggregateException a zachytit všechny, které můžete bezpečně obnovit. Ve výjimečných případech mohou být vyvolány některé výjimky, které nejsou zabaleny v objektu AggregateException, a ThreadAbortExceptions nejsou také zabaleny.
Pokud jsou výjimky povolené, aby se vrátily zpět do spojovacího vlákna, je možné, že dotaz může po vyvolání výjimky pokračovat ve zpracování některých položek.
Další informace naleznete v tématu Postupy: Zpracování výjimek v plINQ dotazu.
Vlastní dělitele
V některých případech můžete zlepšit výkon dotazů tím, že napíšete vlastní dělicí modul, který využívá některé charakteristiky zdrojových dat. V dotazu je vlastní partitioner samotný výčtem objektu, který je dotazován.
int[] arr = new int[9999];
Partitioner<int> partitioner = new MyArrayPartitioner<int>(arr);
var query = partitioner.AsParallel().Select(SomeFunction);
Dim arr(10000) As Integer
Dim partitioner As Partitioner(Of Integer) = New MyArrayPartitioner(Of Integer)(arr)
Dim query = partitioner.AsParallel().Select(Function(x) SomeFunction(x))
PLINQ podporuje pevný počet oddílů (i když se data můžou dynamicky znovu přiřadit k těmto oddílům během doby běhu pro vyrovnávání zatížení).) For a ForEach podporují pouze dynamické dělení, což znamená, že počet oddílů se mění za běhu. Další informace naleznete v tématu Vlastní partitionery pro PLINQ a TPL.
Měření výkonu PLINQ
V mnoha případech může být dotaz paralelizován, ale režie při nastavování paralelního dotazu převáží nad tím, jak se získají výhody výkonu. Pokud dotaz neprovádí mnoho výpočtů nebo pokud je zdroj dat malý, může být dotaz PLINQ pomalejší než sekvenční dotaz LINQ to Objects. Paralelní Analyzátor výkonu v sadě Visual Studio Team Server můžete použít k porovnání výkonu různých dotazů, k vyhledání kritických bodů zpracování a k určení, jestli je váš dotaz spuštěný paralelně nebo postupně. Další informace najdete v tématu Vizualizér souběžnosti a postupy: Měření výkonu dotazů PLINQ.