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.
V mnoha případech může PLINQ poskytovat významná vylepšení výkonu oproti sekvenčním dotazům LINQ to Objects. Práce paralelizace provádění dotazu ale představuje složitost, která může vést k problémům, které v sekvenčním kódu nejsou tak běžné nebo vůbec nejsou zjištěny. Toto téma uvádí některé postupy, kterým se můžete vyhnout při psaní dotazů PLINQ.
Nepředpokládej, že paralelní zpracování je vždy rychlejší než sekvenční.
Paralelizace někdy způsobí, že dotaz PLINQ bude pomalejší než jeho ekvivalent LINQ to Objects. Základním pravidlem je, že dotazy s několika zdrojovými prvky a rychlými delegáty procesu uživatelů pravděpodobně mnoho neurychlí. Vzhledem k tomu, že výkon se týká mnoha faktorů, doporučujeme před rozhodnutím, zda použít PLINQ, měřit skutečné výsledky. Další informace naleznete v tématu Principy zrychlení v PLINQ.
Vyhněte se zápisu do umístění sdílené paměti
V sekvenčním kódu není neobvyklé číst ze statických proměnných nebo zapisovat do polí třídy. Kdykoli však k těmto proměnným současně přistupuje více vláken, existuje velký potenciál pro podmínky souběhu. I když můžete použít zámky k synchronizaci přístupu k proměnné, náklady na synchronizaci můžou poškodit výkon. Proto doporučujeme, abyste se co nejvíce vyhnuli nebo omezili přístup ke sdílenému stavu v dotazu PLINQ.
Příklad: Podmínka závodu se sdílenou pamětí
Následující příklad demonstruje podmínku souběhu, která nastane, když více vláken zapisuje do sdílené proměnné. K proměnné total se přistupuje a současně upravuje více vláken bez synchronizace, což vede k nepředvídatelným výsledkům:
static void DemonstrateRaceCondition()
{
int total = 0;
var numbers = Enumerable.Range(0, 10000);
// UNSAFE: Multiple threads writing to shared variable
numbers.AsParallel().ForAll(n => total += n);
Console.WriteLine($"Total (with race condition): {total}");
// Expected: 49,995,000 but result is unpredictable due to race condition
}
Shared Sub DemonstrateRaceCondition()
Dim total As Integer = 0
Dim numbers = Enumerable.Range(0, 10000)
' UNSAFE: Multiple threads writing to shared variable
numbers.AsParallel().ForAll(Sub(n) total += n)
Console.WriteLine($"Total (with race condition): {total}")
' Expected: 49,995,000 but result is unpredictable due to race condition
End Sub
V tomto kódu není operace total += n atomická. Zahrnuje čtení aktuální hodnoty total, přidání na zápis výsledku zpět do total. Pokud tuto operaci provádí více vláken současně, mohou číst stejnou hodnotu, přidávat do ní v různých vláknech a zapisovat výsledky, které vzájemně přepisují. To vede ke ztrátě některých přírůstků, což vede k nesprávnému konečnému výsledku.
Správným přístupem je použití operací bezpečných pro přístup z více vláken, které nevyžadují sdílený proměnlivý stav:
static void DemonstrateCorrectApproach()
{
var numbers = Enumerable.Range(0, 10000);
// SAFE: Use thread-safe aggregate operation
int total = numbers.AsParallel().Sum();
Console.WriteLine($"Total (correct): {total}");
// Result is always 49,995,000
}
Shared Sub DemonstrateCorrectApproach()
Dim numbers = Enumerable.Range(0, 10000)
' SAFE: Use thread-safe aggregate operation
Dim total As Integer = numbers.AsParallel().Sum()
Console.WriteLine($"Total (correct): {total}")
' Result is always 49,995,000
End Sub
Metoda Sum zpracovává paralelizaci interně bezpečným způsobem s vlákny a zajišťuje správné výsledky bez nutnosti explicitní synchronizace. Mezi další bezpečné přístupy patří použití Aggregate pro vlastní agregace nebo shromažďování výsledků do kolekcí bezpečných pro vlákna, jako je ConcurrentBag<T>.
Vyhněte se nadměrné paralelizaci
Při použití metody AsParallel vznikají režijní náklady spojené s rozdělováním zdrojové kolekce a synchronizací pracovních vláken. Výhody paralelizace jsou dále omezeny počtem procesorů v počítači. Není možné získat žádné zrychlení spuštěním několika vláken vázaných na výpočetní výkon pouze na jednom procesoru. Proto musíte být opatrní, abyste nenadměrně paralelizovali dotaz.
Nejběžnějším scénářem, ve kterém může dojít k nadměrné paralelizaci, je ve vnořených dotazech, jak je znázorněno v následujícím úryvku.
var q = from cust in customers.AsParallel()
from order in cust.Orders.AsParallel()
where order.OrderDate > date
select new { cust, order };
Dim q = From cust In customers.AsParallel()
From order In cust.Orders.AsParallel()
Where order.OrderDate > aDate
Select New With {cust, order}
V tomto případě je nejlepší paralelizovat pouze vnější zdroj dat (zákazníci), pokud neplatí jedna nebo více následujících podmínek:
Vnitřní zdroj dat (cust.Orders) je známo, že je velmi dlouhý.
Provádíte nákladný výpočet pro každou objednávku. (Operace zobrazená v příkladu není nákladná.)
Cílový systém je známý, že má dostatek procesorů pro zpracování počtu vláken, která budou vytvořena paralelizací dotazu na
cust.Orders.
Nejlepší způsob, jak určit optimální tvar dotazu, je ve všech případech testovat a měřit. Další informace naleznete v tématu Postupy: Měření výkonu dotazů PLINQ.
Vyhněte se volání metod, které nejsou bezpečné pro přístup z více vláken
Zápis do metod instancí bez vláken z dotazu PLINQ může vést k poškození dat, které může nebo nemusí být v programu nezjištěno. Může také vést k výjimkám. V následujícím příkladu by se více vláken pokusilo volat metodu FileStream.Write současně, což není podporováno třídou.
Dim fs As FileStream = File.OpenWrite(…)
a.AsParallel().Where(...).OrderBy(...).Select(...).ForAll(Sub(x) fs.Write(x))
FileStream fs = File.OpenWrite(...);
a.AsParallel().Where(...).OrderBy(...).Select(...).ForAll(x => fs.Write(x));
Omezení volání metod bezpečných pro přístup z více vláken
Většina statických metod v .NET je bezpečná pro přístup z více vláken a lze ji volat současně z více vláken. I v těchto případech ale může synchronizace vést k významnému zpomalení dotazu.
Poznámka:
Můžete to otestovat sami vložením některých volání WriteLine do dotazů. I když se tato metoda používá v příkladech dokumentace pro demonstrační účely, nepoužívejte ji v dotazech PLINQ.
Vyhněte se zbytečným operacím řazení
Když PLINQ provede dotaz paralelně, rozdělí zdrojovou sekvenci na oddíly, na které lze pracovat souběžně na více vláknech. Ve výchozím nastavení není pořadí, ve kterém se oddíly zpracovávají a výsledky jsou doručovány, předvídatelné (s výjimkou operátorů, jako je OrderBy). PlINQ můžete instruovat, aby zachovalo pořadí libovolné zdrojové sekvence, ale to má negativní dopad na výkon. Osvědčeným postupem, kdykoli je to možné, je strukturovat dotazy tak, aby se nespoléhaly na zachování objednávek. Další informace naleznete v tématu Zachování objednávek v PLINQ.
Preferujte ForAll před ForEach, pokud je to možné
I když PLINQ spustí dotaz na více vláken, pokud výsledky použijete ve smyčce foreach (For Each v Visual Basic), výsledky dotazu se musí sloučit zpět do jednoho vlákna a přistupovat k němu sériově pomocí enumerátoru. V některých případech je to nevyhnutelné, avšak kdykoli je to možné, použijte metodu ForAll, která umožňuje každému vláknu generovat své vlastní výsledky, například zápisem do kolekce bezpečné pro přístup z více vláken System.Collections.Concurrent.ConcurrentBag<T>.
Stejný problém platí i pro Parallel.ForEach. Jinými slovy, source.AsParallel().Where().ForAll(...) mělo by být důrazně upřednostňované Parallel.ForEach(source.AsParallel().Where(), ...).
Mějte na paměti problémy s afinitou vláken.
Některé technologie, například interoperabilita modelu COM pro komponenty Single-Threaded Apartment (STA), Windows Forms a Windows Presentation Foundation (WPF), ukládají omezení spřažení vláken, která vyžadují spuštění kódu v určitém vlákně. Například v Windows Forms i WPF lze k ovládacímu prvku přistupovat pouze ve vlákně, na kterém byl vytvořen. Pokud se pokusíte získat přístup ke sdílenému stavu ovládacího prvku Windows Forms v dotazu PLINQ, při spuštění v ladicím programu se vyvolá výjimka. (Toto nastavení je možné vypnout.) Pokud je však váš dotaz spotřebován ve vlákně uživatelského rozhraní, můžete získat přístup k ovládacímu prvku ze foreach smyčky, která vyčísluje výsledky dotazu, protože tento kód se spouští pouze na jednom vlákně.
Nepředpokládáme, že iterace forEach, For a ForAll se vždy provádějí paralelně.
Je důležité mít na paměti, že jednotlivé iterace v Parallel.For, Parallel.ForEach nebo ForAll smyčce mohou být, ale nemusí být prováděny paralelně. Proto byste se měli vyhnout psaní jakéhokoli kódu, který závisí na správnosti paralelního provádění iterací nebo na provádění iterací v libovolném konkrétním pořadí.
Například tento kód se pravděpodobně zasekne.
Dim mre = New ManualResetEventSlim()
Enumerable.Range(0, Environment.ProcessorCount * 100).AsParallel().ForAll(Sub(j)
If j = Environment.ProcessorCount Then
Console.WriteLine("Set on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j)
mre.Set()
Else
Console.WriteLine("Waiting on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j)
mre.Wait()
End If
End Sub) ' deadlocks
ManualResetEventSlim mre = new ManualResetEventSlim();
Enumerable.Range(0, Environment.ProcessorCount * 100).AsParallel().ForAll((j) =>
{
if (j == Environment.ProcessorCount)
{
Console.WriteLine("Set on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j);
mre.Set();
}
else
{
Console.WriteLine("Waiting on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j);
mre.Wait();
}
}); //deadlocks
V tomto příkladu jedna iterace nastaví událost a všechny ostatní iterace čekají na událost. Žádná z čekajících iterací se nemůže dokončit, dokud se iterace nastavení událostí nedokončí. Je však možné, že čekající iterace zablokují všechna vlákna použitá k provedení paralelní smyčky, ještě než má iterace určená k nastavení události příležitost se vykonat. Výsledkem je vzájemné zablokování – iterace nastavení událostí se nikdy nespustí a čekající iterace se nikdy neprobudí.
Konkrétně by jedna iterace paralelní smyčky nikdy neměla čekat na další iteraci smyčky, aby se dosáhlo pokroku. Pokud se paralelní smyčka rozhodne naplánovat iterace postupně, ale v opačném pořadí, dojde k zablokování.