Potenciální nástrahy datového a funkčního paralelismu

V mnoha případech Parallel.For může Parallel.ForEach výrazně zlepšit výkon u běžných sekvenčních smyček. Práce paralelizace smyčky 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 obsahuje několik postupů, které byste se měli vyhnout při psaní paralelních smyček.

Nepředpokládáme, že paralelní je vždy rychlejší

V některých případech může paralelní smyčka běžet pomaleji než její sekvenční ekvivalent. Základním pravidlem je, že paralelní smyčky, které mají málo iterací a rychlých delegátů uživatelů, se pravděpodobně příliš urychlí. Vzhledem k tomu, že výkon se týká mnoha faktorů, doporučujeme vždy měřit skutečné výsledky.

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 paralelní smyčce. Nejlepším způsobem, jak to provést, je použít přetížení Parallel.For a Parallel.ForEach které používají proměnnou System.Threading.ThreadLocal<T> k uložení stavu vlákna-místní během provádění smyčky. Další informace najdete v tématu Postupy: Zápis smyčky Parallel.For s využitím místních proměnných vláken a postupy: Zápis smyčky Parallel.ForEach pomocí místních proměnných oddílů.

Vyhněte se nadměrné paralelizaci

Pomocí paralelních smyček získáte režijní náklady na dělení zdrojové kolekce a synchronizaci 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 smyčku.

Nejběžnějším scénářem, ve kterém může dojít k nadměrné paralelizaci, je vnořených smyček. Ve většině případů je nejlepší paralelizovat pouze vnější smyčku, pokud neplatí jedna nebo více následujících podmínek:

  • Vnitřní smyčka 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.

Vyhněte se volání metod Sejf bez vláken

Zápis do metod instancí bezpečných bez vláken z paralelní smyčky 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.WriteByte současně, což není podporováno třídou.

FileStream fs = File.OpenWrite(path);
byte[] bytes = new Byte[10000000];
// ...
Parallel.For(0, bytes.Length, (i) => fs.WriteByte(bytes[i]));
Dim fs As FileStream = File.OpenWrite(filepath)
Dim bytes() As Byte
ReDim bytes(1000000)
' ...init byte array
Parallel.For(0, bytes.Length, Sub(n) fs.WriteByte(bytes(n)))

Omezení volání metod Sejf 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 k demonstračním účelům, nepoužívejte ji v paralelních smyčkách, pokud to není nutné.

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. To znamená, že například nelze aktualizovat ovládací prvek seznamu z paralelní smyčky, pokud nenakonfigurujete plánovač vláken tak, aby plánovat práci pouze na vlákně uživatelského rozhraní. Další informace naleznete v tématu Určení kontextu synchronizace.

Při čekání na delegáty, které volá parallel.Invoke, používejte upozornění.

Za určitých okolností bude paralelní knihovna úloh vloženého úkolu, což znamená, že běží na úkolu v aktuálně běžícím vlákně. (Další informace najdete v tématu Plánovače úloh.) Tato optimalizace výkonu může v určitých případech vést k zablokování. Například dva úkoly můžou spustit stejný kód delegáta, který signalizuje, kdy dojde k události, a pak čeká na signál druhého úkolu. Pokud je druhý úkol vložen ve stejném vlákně jako první a první přejde do stavu Čekání, druhý úkol nebude nikdy schopen signalizovat událost. Chcete-li se takovému výskytu vyhnout, můžete zadat časový limit operace Čekání nebo pomocí explicitních konstruktorů vláken zajistit, aby jedna úloha nemohla blokovat druhou.

Nepředpokládáme, že se iterace forEach, For a ForAll vždy spouští paralelně.

Je důležité mít na paměti, že jednotlivé iterace v sadě For, ForEach nebo ForAll smyčka mohou, ale nemusí se spouště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:

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
Dim mres = 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)
                mres.Set()
            Else
                Console.WriteLine("Waiting on {0} with value of {1}",
                                  Thread.CurrentThread.ManagedThreadId, j)
                mres.Wait()
            End If
        End Sub) ' 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í.

Vyhněte se provádění paralelních smyček ve vlákně uživatelského rozhraní

Je důležité zachovat odezvu uživatelského rozhraní aplikace. Pokud operace obsahuje dostatek práce na povolení paralelizace, pravděpodobně by neměla být spuštěna ve vlákně uživatelského rozhraní. Místo toho by měla tuto operaci přesměrovat, aby byla spuštěna na vlákně na pozadí. Pokud například chcete použít paralelní smyčku k výpočtu některých dat, která by se pak měla vykreslit do ovládacího prvku uživatelského rozhraní, měli byste zvážit spuštění smyčky v instanci úlohy, nikoli přímo v obslužné rutině události uživatelského rozhraní. Teprve po dokončení základního výpočtu byste pak měli uživatelské rozhraní zařaďte zpět do vlákna uživatelského rozhraní.

Pokud spouštíte paralelní smyčky ve vlákně uživatelského rozhraní, dávejte pozor, abyste se vyhnuli aktualizaci ovládacích prvků uživatelského rozhraní ve smyčce. Pokus o aktualizaci ovládacích prvků uživatelského rozhraní z paralelní smyčky, která se spouští ve vlákně uživatelského rozhraní, může vést k poškození stavu, výjimky, zpožděné aktualizace a dokonce zablokování v závislosti na způsobu vyvolání aktualizace uživatelského rozhraní. V následujícím příkladu paralelní smyčka blokuje vlákno uživatelského rozhraní, na kterém se spouští, dokud nebudou dokončeny všechny iterace. Pokud je však iterace smyčky spuštěna ve vlákně na pozadí (jak For je to možné), volání vyvolání způsobí odeslání zprávy do vlákna uživatelského rozhraní a blokuje čekání na zpracování této zprávy. Vzhledem k tomu, že vlákno uživatelského rozhraní je blokováno spuštěním For, zpráva nemůže být nikdy zpracována a vlákno uživatelského rozhraní zablokování.

private void button1_Click(object sender, EventArgs e)
{
    Parallel.For(0, N, i =>
    {
        // do work for i
        button1.Invoke((Action)delegate { DisplayProgress(i); });
    });
}
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

    Dim iterations As Integer = 20
    Parallel.For(0, iterations, Sub(x)
                                    Button1.Invoke(Sub()
                                                       DisplayProgress(x)
                                                   End Sub)
                                End Sub)
End Sub

Následující příklad ukazuje, jak se vyhnout vzájemnému zablokování spuštěním smyčky uvnitř instance úlohy. Vlákno uživatelského rozhraní není zablokované smyčkou a zprávu lze zpracovat.

private void button1_Click(object sender, EventArgs e)
{
    Task.Factory.StartNew(() =>
        Parallel.For(0, N, i =>
        {
            // do work for i
            button1.Invoke((Action)delegate { DisplayProgress(i); });
        })
         );
}
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

    Dim iterations As Integer = 20
    Task.Factory.StartNew(Sub() Parallel.For(0, iterations, Sub(x)
                                                                Button1.Invoke(Sub()
                                                                                   DisplayProgress(x)
                                                                               End Sub)
                                                            End Sub))
End Sub

Viz také