Sdílet prostřednictvím


Potenciální nástrahy s PLINQ

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ádáme, že paralelní je vždy rychlejší.

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 uživatelů pravděpodobně příliš urychlí. 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 polích třídy nebo zapisovat z něj. Kdykoli však k těmto proměnným současně přistupuje více vláken, existuje velký potenciál pro podmínky časování. 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.

Vyhněte se nadměrné paralelizaci

AsParallel Pomocí této metody se účtují režijní náklady na dělení zdrojové kolekce a synchronizace 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 nepřekončili paralelizaci dotazu.

Nejběžnějším scénářem, ve kterém může dojít k nadměrné paralelizaci, je vnořených dotazech, jak je znázorněno v následujícím fragmentu kódu.

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. Objednávky) je známo, že jsou 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šinastatických 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í do WriteLine 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čeny, předvídatelné (s výjimkou operátorů, jako OrderByjsou ). 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 forEach, pokud je to možné

I když PLINQ spustí dotaz na více vláken, pokud použijete výsledky ve foreach smyčce (For Each v jazyce 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 to není možné; kdykoli je to však možné, použijte metodu ForAll , která umožňuje každému vláknu výstupu vlastní výsledky, například zápisem do kolekce bezpečné pro přístup z více vláken, například 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 se spřažením vláken.

Některé technologie, například interoperabilita modelu COM pro komponenty STA (Single-Threaded Apartment), model Windows Forms a Windows Presentation Foundation (WPF), ukládají omezení spřažení vláken, která vyžadují, aby kód běžel na konkrétním vlákně. Například v model 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 model 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.ForParallel.ForEachnebo ForAll smyčka mohou, ale nemusí provádět 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 pravděpodobně zablokuje:

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 blokují všechna vlákna, která se používají ke spuštění paralelní smyčky, než iterace nastavení událostí měla šanci provést. 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 neměla čekat na další iteraci smyčky, aby se postup pokroku. Pokud se paralelní smyčka rozhodne naplánovat iterace postupně, ale v opačném pořadí, dojde k zablokování.

Viz také