Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
In veel gevallen Parallel.For en Parallel.ForEach kunnen aanzienlijke prestatieverbeteringen bieden ten opzichte van gewone opeenvolgende lussen. Het werk van het parallelliseren van de lus introduceert echter complexiteit die kan leiden tot problemen die, in sequentiële code, niet zo gebruikelijk zijn of helemaal niet worden aangetroffen. In dit artikel worden enkele praktijken vermeld die u moet vermijden bij het schrijven van parallelle lussen.
Ga er niet van uit dat parallel altijd sneller is
In bepaalde gevallen kan een parallelle lus langzamer worden uitgevoerd dan het sequentiële equivalent. De vuistregel is dat parallelle lussen met weinig iteraties en snelle gebruikersfuncties waarschijnlijk niet veel sneller zullen gaan. Omdat er echter veel factoren bij de prestaties betrokken zijn, raden we u aan de werkelijke resultaten altijd te meten.
Vermijd schrijven naar gedeelde geheugenlocaties
In sequentiële code is het niet ongebruikelijk om statische variabelen of klassevelden te lezen of te schrijven. Wanneer meerdere threads echter gelijktijdig toegang hebben tot dergelijke variabelen, is er een groot potentieel voor racevoorwaarden. Hoewel u vergrendelingen kunt gebruiken om de toegang tot de variabele te synchroniseren, kunnen de kosten van synchronisatie de prestaties schaden. Daarom raden we u aan de toegang tot gedeelde status in een parallelle lus zoveel mogelijk te vermijden of ten minste te beperken. De beste manier om dit te doen, is door de overloads van Parallel.For en Parallel.ForEach te gebruiken, die een System.Threading.ThreadLocal<T>-variabele gebruiken om de status per thread op te slaan tijdens de uitvoering van de lus. Voor meer informatie, zie Hoe: Schrijf een Parallel.For-lus met thread-lokale variabelen en Hoe: Schrijf een Parallel.ForEach-lus met partitie-lokale variabelen.
Overparallellisatie voorkomen
Door parallelle lussen te gebruiken, worden de overheadkosten in rekening gebracht voor het partitioneren van de bronverzameling en het synchroniseren van de werkrolthreads. De voordelen van parallelle uitvoering worden verder beperkt door het aantal processors op de computer. Er is geen versnelling te behalen door meerdere compute-gebonden threads uit te voeren op slechts één processor. Daarom moet u voorzichtig zijn een lus niet te veel te parallelliseren.
Het meest voorkomende scenario waarin overparallellisatie kan optreden, bevindt zich in geneste lussen. In de meeste gevallen kunt u het beste alleen de buitenste lus parallelliseren, tenzij een of meer van de volgende voorwaarden van toepassing zijn:
De binnenste lus is bekend erg lang.
U voert een dure berekening uit op elke order. (De bewerking die in het voorbeeld wordt weergegeven, is niet duur.)
Het doelsysteem heeft voldoende processors om het aantal threads aan te kunnen dat wordt gegenereerd door parallelle verwerking.
In alle gevallen kunt u de optimale queryvorm het beste testen en meten.
Vermijd aanroepen naar niet-threadveilige methoden
Schrijven naar niet-thread-veilige instantiemethoden vanuit een parallelle loop kan leiden tot gegevenscorruptie die mogelijk niet wordt opgemerkt in uw programma. Het kan ook leiden tot uitzonderingen. In het volgende voorbeeld proberen meerdere threads tegelijkertijd de FileStream.WriteByte methode aan te roepen, die niet wordt ondersteund door de klasse.
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)))
Aanroepen beperken tot threadveilige methoden
De meeste statische methoden in .NET zijn thread-veilig en kunnen gelijktijdig vanuit meerdere threads worden aangeroepen. Zelfs in deze gevallen kan de betrokken synchronisatie echter leiden tot een aanzienlijke vertraging in de query.
Notitie
U kunt dit zelf testen door enkele aanroepen aan uw queries toe te voegen door WriteLine in te voegen. Hoewel deze methode wordt gebruikt in de documentatievoorbeelden voor demonstratiedoeleinden, moet u deze niet in parallelle lussen gebruiken, tenzij dat nodig is.
Wees bewust van problemen met threadaffiniteit
Sommige technologieën, zoals COM-interoperabiliteit voor STA-onderdelen (Single Threaded Apartment), Windows Forms en Windows Presentation Foundation (WPF), leggen threadaffiniteitsbeperkingen op waarvoor code moet worden uitgevoerd op een specifieke thread. In Zowel Windows Forms als WPF kan een besturingselement bijvoorbeeld alleen worden geopend op de thread waarop het is gemaakt. Dit betekent bijvoorbeeld dat u een lijstbeheer niet kunt bijwerken vanuit een parallelle lus, tenzij u de threadplanner configureert om alleen werk te plannen op de UI-thread. Zie Een synchronisatiecontext opgeven voor meer informatie.
Wees voorzichtig bij het wachten op delegees die worden aangeroepen door Parallel.Invoke.
Onder bepaalde omstandigheden zal de taakparallelbibliotheek een taak inline uitvoeren, wat betekent dat deze wordt uitgevoerd op de momenteel actieve thread. (Zie voor meer informatie Taakplanners.) Deze prestatieoptimalisatie kan in bepaalde gevallen leiden tot impasses. Twee taken kunnen bijvoorbeeld dezelfde gedelegeerde code uitvoeren, die aangeeft wanneer een gebeurtenis plaatsvindt en vervolgens wacht totdat de andere taak dat signaleert. Als de tweede taak inline is op dezelfde thread als de eerste en de eerste de status Wacht krijgt, kan de tweede taak de gebeurtenis nooit signaleren. Als u een dergelijke gebeurtenis wilt voorkomen, kunt u een time-out opgeven voor de wachtbewerking of expliciete threadconstructors gebruiken om ervoor te zorgen dat de ene taak de andere taak niet kan blokkeren.
Ga er niet van uit dat iteraties van ForEach, For en ForAll Always Parallel worden uitgevoerd
Het is belangrijk om in gedachten te houden dat afzonderlijke iteraties in een For, ForEach of ForAll lus mogelijk, maar niet parallel hoeven uit te voeren. Daarom moet u voorkomen dat u code schrijft die afhankelijk is van juistheid van parallelle uitvoering van iteraties of van de uitvoering van iteraties in een bepaalde volgorde. Deze code zal bijvoorbeeld waarschijnlijk vastlopen.
ManualResetEventSlim mre = new ManualResetEventSlim();
Enumerable.Range(0, Environment.ProcessorCount * 100)
.AsParallel()
.ForAll((j) =>
{
if (j == Environment.ProcessorCount)
{
Console.WriteLine($"Set on {Thread.CurrentThread.ManagedThreadId} with value of {j}");
mre.Set();
}
else
{
Console.WriteLine($"Waiting on {Thread.CurrentThread.ManagedThreadId} with value of {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
In dit voorbeeld wordt met één iteratie een gebeurtenis ingesteld en wachten alle andere iteraties op de gebeurtenis. Geen van de wachtende iteraties kan worden voltooid totdat de iteratie voor het instellen van het evenement is voltooid. Het is echter mogelijk dat de wachtende iteraties alle threads blokkeren die worden gebruikt om de parallelle lus uit te voeren, voordat de iteratie die het evenement instelt de kans heeft gehad om uitgevoerd te worden. Dit resulteert in een impasse: de iteratie voor gebeurtenisinstelling wordt nooit uitgevoerd en de wachtende iteraties worden nooit geactiveerd.
In het bijzonder moet een iteratie van een parallelle lus nooit wachten op een andere iteratie van de lus om vooruitgang te boeken. Als de parallelle lus besluit om de iteraties opeenvolgend te plannen, maar in de omgekeerde volgorde, treedt er een impasse op.
Vermijd het uitvoeren van parallelle lussen op de gebruikersinterface-thread.
Het is belangrijk om de gebruikersinterface (UI) van uw toepassing responsief te houden. Als een bewerking voldoende werk bevat om parallellisatie te rechtvaardigen, moet deze waarschijnlijk niet worden uitgevoerd op de UI-thread. In plaats daarvan moet deze bewerking worden overgedragen om uitgevoerd te worden in een achtergrondproces. Als u bijvoorbeeld een parallelle lus wilt gebruiken om bepaalde gegevens te berekenen die vervolgens in een UI-besturingselement moeten worden weergegeven, kunt u overwegen om de lus binnen een taakexemplaren uit te voeren in plaats van rechtstreeks in een UI-gebeurtenis-handler. Alleen wanneer de kernberekening is voltooid, moet u de UI-update vervolgens weer naar de UI-thread uitvoeren.
Als u parallelle lussen uitvoert op de UI-thread, moet u voorkomen dat u ui-besturingselementen vanuit de lus bijwerkt. Als u ui-besturingselementen probeert bij te werken vanuit een parallelle lus die wordt uitgevoerd op de UI-thread, kan dit leiden tot statusbeschadiging, uitzonderingen, vertraagde updates en zelfs impasses, afhankelijk van hoe de UI-update wordt aangeroepen. In het volgende voorbeeld blokkeert de parallelle lus de UI-thread waarop deze wordt uitgevoerd totdat alle iteraties zijn voltooid. Als een iteratie van de lus echter wordt uitgevoerd op een achtergrondthread (zoals For mogelijk doet), zorgt de aanroep naar Invoke ervoor dat een bericht wordt verzonden naar de UI-thread en blokkeert terwijl het wacht tot dat bericht wordt verwerkt. Omdat de UI-thread wordt geblokkeerd bij het uitvoeren van de For, kan het bericht nooit worden verwerkt en kan de UI-thread in een deadlock raken.
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
In het volgende voorbeeld ziet u hoe u de impasse kunt voorkomen door de lus binnen een taakexemplaar uit te voeren. De UI-thread wordt niet geblokkeerd door de lus en het bericht kan worden verwerkt.
private void button2_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