Share via


Mogelijke valkuilen in gegevens- en taakparallellisme

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 onderwerp vindt u enkele procedures om te voorkomen wanneer u parallelle lussen schrijft.

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 basisregel van duim is dat parallelle lussen met weinig iteraties en snelle gebruikersdelegaties waarschijnlijk niet veel sneller zullen worden. Omdat er echter veel factoren bij de prestaties betrokken zijn, raden we u aan de werkelijke resultaten altijd te meten.

Schrijf naar gedeelde geheugenlocaties voorkomen

In sequentiële code is het niet ongebruikelijk dat u statische variabelen of klassevelden leest of schrijft. 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 gebruik te maken van de overbelastingen van Parallel.For en Parallel.ForEach die een System.Threading.ThreadLocal<T> variabele gebruiken om thread-lokale status op te slaan tijdens het uitvoeren van een lus. Zie How to: Write a Parallel.For Loop with Thread-Local Variables and How to: Write a Parallel.ForEach Loop with Partition-Local Variables voor meer informatie.

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 om een lus niet 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 is bekend dat er voldoende processors zijn om het aantal threads te verwerken dat wordt geproduceerd door de query cust.Ordersparallel te maken.

In alle gevallen kunt u de optimale queryshape het beste testen en meten.

Vermijd aanroepen naar niet-threadveilige methoden

Schrijven naar niet-thread-veilige exemplaarmethoden vanuit een parallelle lus kan leiden tot beschadiging van gegevens die mogelijk niet worden gedetecteerd 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 in uw query's in te WriteLine voegen. Hoewel deze methode wordt gebruikt in de documentatievoorbeelden voor demonstratiedoeleinden, moet u deze niet in parallelle lussen gebruiken, tenzij dat nodig is.

Houd rekening met 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 in gemachtigden die worden aangeroepen door Parallel.Invoke

In bepaalde omstandigheden wordt in de taakparallelbibliotheek een taak inline geplaatst, wat betekent dat deze wordt uitgevoerd op de taak op het moment dat de thread wordt uitgevoerd. (Zie voor meer informatie Taakplanners.) Deze prestatieoptimalisatie kan in bepaalde gevallen leiden tot impasses. Twee taken kunnen bijvoorbeeld dezelfde delegeringscode uitvoeren, die aangeeft wanneer een gebeurtenis plaatsvindt en vervolgens wacht tot de andere taak wordt gesignaleerde. 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 is bijvoorbeeld waarschijnlijk een impasse:

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

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 van de gebeurtenis-instelling 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 van de gebeurtenis-instelling de kans heeft gehad om uit te voeren. 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 parallelle lussen uitvoeren op de UI-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 offload om te worden uitgevoerd op een achtergrondthread. 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 wel), zorgt de aanroep voor aanroepen ervoor dat een bericht wordt verzonden naar de UI-thread en blokken die wachten tot dat bericht wordt verwerkt. Omdat de UI-thread wordt geblokkeerd, Forkan het bericht nooit worden verwerkt en kan de ui-thread impasses niet worden verwerkt.

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 in een taakexemplaren uit te voeren. De UI-thread wordt niet geblokkeerd door de lus en het bericht kan worden verwerkt.

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

Zie ook