Potenciální nástrahy datového a funkčního paralelismu
V mnoha případech mohou Parallel.For a Parallel.ForEach poskytnout významné výkonnostní zlepšení oproti běžným sekvenčním smyčkám. Vytváření paralelní smyčky však zavádí složitost, která může vést k problémům, které nejsou v sekvenčním kódu tak běžné nebo se nevyskytují vůbec. V tomto tématu jsou uvedeny některé postupy, kterým je třeba se při psaní paralelní smyčky vyhnout.
Nepředpokládat, že paralelní vždy znamená rychlejší
V některých případech může paralelní smyčka běžet pomaleji než sekvenční ekvivalent. Základním pravidlem je, že paralelní smyčky, které mají jen několik iterací a rychlé uživatelské delegáty, není možné příliš zrychlit. Protože však výkon ovlivňuje mnoho faktorů, doporučujeme vždy změřit skutečné výsledky.
Vyhnout se zápisu do umístění sdílené paměti
V sekvenčním kódu je běžné číst z nebo zapisovat do statických proměnných nebo polí tříd. Ovšem pokaždé, když více vláken přistupuje k takovýmto proměnným najednou, je vysoká pravděpodobnost výskytu souběhu. Přestože lze přístup k proměnné synchronizovat pomocí uzamykání, náklady na synchronizaci mohou snížit výkon. Proto doporučujeme předcházet nebo alespoň co nejvíce v paralelních smyčkách omezit přístup ke sdílenému stavu. Nejlepším přístupem je použít přetížení smyček Parallel.For a Parallel.ForEach, které používají proměnnou System.Threading.ThreadLocal<T> pro uložení místního stavu vlákna během provádění smyčky. Další informace naleznete v tématu Postupy: Zápis Parallel.For smyčky, který byl podproces místní proměnné a Postupy: Zápis Parallel.ForEach smyčky, který byl podproces místní proměnné.
Vyhnout se nadbytečnému paralelnímu zpracování
Při použití paralelních smyček se vynakládá režie na rozdělení zdrojové kolekce a synchronizaci pracovních vláken. Výhody paralelního zpracování jsou dále omezeny počtem procesorů v počítači. Spuštěním více výpočetních vláken na jediném procesoru nelze dosáhnout žádného zrychlení. Proto je třeba dbát na to, aby nebyla smyčka paralelní nadbytečně.
Nejběžnější scénář nadbytečného paralelního zpracování může nastat ve vnořených smyčkách. Ve většině případů je nejlepší učinit paralelní pouze vnější smyčku, pokud nenastane jedna nebo více z následujících podmínek:
Je známo, že vnitřní smyčka je velmi dlouhá.
Jsou prováděny nákladné výpočty pro každou objednávku. (Operace v příkladu není nákladná.)
Cílový systém má dostatek procesorů, aby zpracoval daný počet vláken, které budou vytvořeny paralelním prováděním dotazu nad cust.Orders.
Ve všech případech je nejlepší určit optimální tvar dotazu pomocí testů a měření.
Zabránit volání metod, které nejsou zabezpečeny pro přístup z více vláken
Zápis z paralelní smyčky do instančních metod, které nejsou zabezpečeny pro přístup z více vláken, může vést k poškození dat, které může nebo nemusí být programem rozpoznáno. Toto také může vést k výjimkám. V následujícím příkladu by více podprocesů pokouší volat FileStream.WriteByte Metoda současně, která není podporována třída
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)))
FileStream fs = File.OpenWrite(path);
byte[] bytes = new Byte[10000000];
// ...
Parallel.For(0, bytes.Length, (i) => fs.WriteByte(bytes[i]));
Omezit volání metod zabezpečených pro přístup z více vláken
Většina statických metod v rozhraní .NET Framework jsou zabezpečeny pro přístup z více vláken a lze je tedy volat z více vláken souběžně. I v těchto případech však zapojení synchronizace může vést k významnému zpomalení dotazu.
![]() |
---|
Toto můžete vyzkoušet sami vložením několika volání WriteLine do vašich dotazů.Přestože tato metoda se používá v příkladech dokumentace pro demonstrační účely, nepoužívejte ji v paralelní vedení nezbytné. |
Vyhnout se chyb spojených se spřažením vláken
Některé technologie, jako například vzájemná funkční spolupráce modelu COM pro komponenty Single-Threaded Apartment (STA), model Windows Forms a Windows Presentation Foundation (WPF), zavádí omezení na spřažení vláken vyžadující spouštění kódu v určitém vlákně. Například jak v modelu Windows Forms, tak i ve WPF lze k ovládacím prvkům získat přístup pouze z vlákna, ve kterém byl tento prvek vytvořen. To znamená, že například ovládací prvek seznam nelze aktualizovat z paralelní smyčky, pokud není plánovač vláken nakonfigurován k plánování práce pouze na vlákno uživatelského rozhraní. Další informace naleznete v tématu Postupy: Plán práce na zadané synchronizační kontext.
Dbát opatrnosti při čekání v delegátech, kteří jsou voláni metodou Parallel.Invoke
Za určitých okolností vytvoří knihovna Task Parallel Library vloženou úlohu. To znamená, že úlohu spustí v aktuálně prováděném vlákně. (Další informace naleznete 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 dvě úlohy mohou spouštět kód stejného delegáta, který signalizuje, že došlo k nějaké události, a poté čeká na signál druhé úlohy. Pokud je druhá úloha vložená ve stejném vlákně jako první a první přejde do stavu Wait (čekání), druhé úloze nebude nikdy umožněno signalizovat svou událost. Chcete-li se takovýmto případům vyhnout, můžete zadat časový limit kladený na operaci Wait nebo použit explicitní konstruktory vlákna, aby bylo zajištěno, že jedna úloha nemůže blokovat druhou.
Nepředpokládat, že iterace ForEach, For a ForAll budou vždy prováděny paralelně
Je důležité mít na paměti, že jednotlivé iterace ve smyčkách For, ForEach a ForAll<TSource> mohou, ale nemusí být prováděny paralelně. Proto je třeba se vyhnout psaní kódu, jehož správnost závisí na paralelním provádění iterací nebo na určitém pořadí prováděných iterací. Například v tomto kódu pravděpodobně dojde k zablokování:
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
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 vyvolá událost a všechny ostatní iterace na událost čekají. Žádná z čekajících iterací nemůže být dokončena, dokud se nedokončí iterace vyvolávající událost. Je však možné, že jedna z čekajících iterací blokuje všechna vlákna, která slouží k provádění paralelní smyčky, ještě před tím, než má iterace vyvolávající událost příležitost se provést. To má za následek zablokování – iterace vyvolávající událost se nikdy neprovede a čekající iterace se nikdy neprobudí.
Jedna iterace paralelní smyčky by nikdy pro své provedení neměla čekat na jinou iteraci smyčky. Pokud se paralelní smyčka rozhodne naplánovat iterace sekvenčně, avšak v opačném pořadí, dojde k zablokování.
Vyhnout 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í (UI) aplikace. Pokud operace obsahuje dostatek práce, aby byla vhodná pro paralelní spouštění, pak by pravděpodobně neměla být prováděna vláknem uživatelského rozhraní. Místo toho by tuto operaci mělo převzít vlákno běžící na pozadí. Například pokud chcete použít paralelní smyčku k výpočtu nějakých dat, která poté mají být vykreslena do ovládacího prvku uživatelského rozhraní, měli byste raději zvážit provádění smyčky v instanci úlohy, než přímo v obslužné rutině uživatelského rozhraní. Pak pouze ve chvíli, kdy jsou výpočty dokončeny, je vyvolána aktualizace uživatelského rozhraní zpět ve svém vlákně.
Pokud jsou ve vlákně uživatelského rozhraní spouštěny paralelní smyčky, je potřeba zabránit aktualizaci ovládacích prvků uživatelského rozhraní uvnitř smyčky. Pokusy o aktualizaci ovládacích prvků uživatelského rozhraní z paralelní smyčky, která se provádí ve vlákně uživatelského rozhraní, může vést k poškození stavu, výjimkám, opožděné aktualizaci či dokonce zablokování, podle toho, jak je vyvolána aktualizace uživatelského rozhraní. V následujícím příkladu blokuje paralelní smyčka vlákno uživatelského rozhraní, ve kterém je prováděna, dokud nejsou dokončeny všechny iterace. Pokud ovšem iterace smyčky běží ve vlákně na pozadí (což For může), volání metody Invoke způsobí, že je do vlákna uživatelského rozhraní zaslána zpráva a čeká se, až bude tato zpráva zpracována. Vzhledem k tomu, že vlákno uživatelského rozhraní je blokováno spouštěním For, nikdy nelze zpracovat zprávu a vlákno uživatelského rozhraní je zablokováno.
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
private void button1_Click(object sender, EventArgs e)
{
Parallel.For(0, N, i =>
{
// do work for i
button1.Invoke((Action)delegate { DisplayProgress(i); });
});
}
Následující příklad ukazuje, jak předejít zablokování spuštěním smyčky uvnitř instance úlohy. Vlákno uživatelského rozhraní není blokováno smyčkou a zpráva může být zpracována.
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
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); });
})
);
}
Viz také
Koncepty
Paralelní programování v rozhraní .NET Framework
Další zdroje
Vzorky pro paralelní programování: Pochopení a použití paralelní vzorky s.NET Framework 4