Dela via


Potentiella fallgropar i data och uppgiftsparallellitet

I många fall Parallel.For och Parallel.ForEach kan ge betydande prestandaförbättringar jämfört med vanliga sekventiella loopar. Arbetet med att parallellisera loopen medför dock komplexitet som kan leda till problem som i sekventiell kod inte är lika vanliga eller inte påträffas alls. Det här avsnittet innehåller några metoder som du kan undvika när du skriver parallella loopar.

Anta inte att parallellen alltid är snabbare

I vissa fall kan en parallell loop köras långsammare än dess sekventiella motsvarighet. Den grundläggande tumregeln är att parallella loopar som har få iterationer och snabba användardelegater sannolikt inte kommer att påskynda mycket. Men eftersom många faktorer är inblandade i prestanda rekommenderar vi att du alltid mäter faktiska resultat.

Undvik att skriva till delade minnesplatser

I sekventiell kod är det inte ovanligt att läsa från eller skriva till statiska variabler eller klassfält. Men när flera trådar kommer åt sådana variabler samtidigt finns det en stor potential för tävlingsförhållanden. Även om du kan använda lås för att synkronisera åtkomsten till variabeln kan kostnaden för synkronisering skada prestanda. Därför rekommenderar vi att du undviker, eller åtminstone begränsar, åtkomst till delat tillstånd i en parallell loop så mycket som möjligt. Det bästa sättet att göra detta är att använda överlagringar av Parallel.For och Parallel.ForEach som använder en System.Threading.ThreadLocal<T> variabel för att lagra trådlokalt tillstånd under loopkörningen. Mer information finns i How to: Write a Parallel.For Loop with Thread-Local Variables and How to: Write a Parallel.ForEach Loop with Partition-Local Variables (Så här skriver du en Parallel.ForEach-loop med partitionslokala variabler).

Undvik överparallellisering

Genom att använda parallella loopar medför du kostnader för att partitionera källsamlingen och synkronisera arbetstrådarna. Fördelarna med parallellisering begränsas ytterligare av antalet processorer på datorn. Det går inte att få någon hastighet genom att köra flera beräkningsbundna trådar på bara en processor. Därför måste du vara noga med att inte överparallellisera en loop.

Det vanligaste scenariot där överparallellisering kan ske är i kapslade loopar. I de flesta fall är det bäst att parallellisera endast den yttre loopen om inte ett eller flera av följande villkor gäller:

  • Den inre slingan är känd för att vara mycket lång.

  • Du utför en dyr beräkning på varje beställning. (Åtgärden som visas i exemplet är inte dyr.)

  • Målsystemet är känt för att ha tillräckligt med processorer för att hantera antalet trådar som skapas genom parallellisering av bearbetningen.

I samtliga fall är det bästa sättet att fastställa den optimala frågeformen att testa och mäta.

Undvik anrop till icke-trådsäkra metoder

Att skriva till instansmetoder som inte är trådsäkra från en parallell loop kan leda till att data skadas, vilket kan leda till att de inte identifieras i programmet. Det kan också leda till undantag. I följande exempel försöker flera trådar anropa FileStream.WriteByte metoden samtidigt, vilket inte stöds av klassen.

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)))

Begränsa anrop till trådsäkra metoder

De flesta statiska metoder i .NET är trådsäkra och kan anropas från flera trådar samtidigt. Men även i dessa fall kan den aktuella synkroniseringen leda till en betydande avmattning i frågan.

Kommentar

Du kan testa detta själv genom att infoga några anrop till WriteLine i dina frågor. Även om den här metoden används i dokumentationsexemplen i demonstrationssyfte ska du inte använda den i parallella loopar om det inte behövs.

Var medveten om problem med trådtillhörighet

Vissa tekniker, till exempel COM-samverkan för STA-komponenter (Single-Threaded Apartment), Windows Forms och Windows Presentation Foundation (WPF), medför trådtillhörighetsbegränsningar som kräver att kod körs på en specifik tråd. I både Windows Forms och WPF kan du till exempel bara komma åt en kontroll i tråden som den skapades på. Det innebär till exempel att du inte kan uppdatera en listkontroll från en parallell loop om du inte konfigurerar trådschemaläggaren så att den endast schemalägger arbete i användargränssnittstråden. Mer information finns i Ange en synkroniseringskontext.

Var försiktig när du väntar i ombud som anropas av Parallel.Invoke

Under vissa omständigheter infogar aktivitetsparallellt bibliotek en aktivitet, vilket innebär att den körs på aktiviteten i den tråd som körs just nu. (Mer information finns i Schemaläggare för aktiviteter.) Den här prestandaoptimeringen kan leda till dödläge i vissa fall. Två aktiviteter kan till exempel köra samma ombudskod, som signalerar när en händelse inträffar, och väntar sedan på att den andra aktiviteten ska signalera. Om den andra aktiviteten är inlindad i samma tråd som den första, och den första hamnar i väntetillstånd, kommer den andra aktiviteten aldrig att kunna signalera händelsen. Om du vill undvika en sådan förekomst kan du ange en tidsgräns för wait-åtgärden eller använda explicita trådkonstruktorer för att säkerställa att den ena uppgiften inte kan blockera den andra.

Anta inte att iterationer av ForEach, For och ForAll alltid körs parallellt

Det är viktigt att komma ihåg att enskilda iterationer i en For, ForEach eller ForAll -loop kan men inte behöver köras parallellt. Därför bör du undvika att skriva någon kod som är beroende av korrekthet vid parallell körning av iterationer eller körning av iterationer i någon viss ordning. Den här koden kommer till exempel sannolikt att vara låst:

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

I det här exemplet anger en iteration en händelse och alla andra iterationer väntar på händelsen. Ingen av de väntande iterationerna kan slutföras förrän iterationen för händelseinställningen har slutförts. Det är dock möjligt att de väntande iterationerna blockerar alla trådar som används för att köra den parallella loopen innan iterationen för händelseinställningen har haft en chans att köras. Detta resulterar i ett dödläge – iterationen för händelseinställningen kommer aldrig att köras och de väntande iterationerna kommer aldrig att vakna.

I synnerhet bör en iteration av en parallell loop aldrig vänta på en annan iteration av loopen för att göra framsteg. Om den parallella loopen bestämmer sig för att schemalägga iterationerna sekventiellt, men i motsatt ordning, uppstår ett dödläge.

Undvik att köra parallella loopar i användargränssnittstråden

Det är viktigt att hålla programmets användargränssnitt (UI) responsivt. Om en åtgärd innehåller tillräckligt med arbete för att motivera parallellisering bör den sannolikt inte köras i användargränssnittstråden. I stället bör åtgärden avlastas för att köras på en bakgrundstråd. Om du till exempel vill använda en parallell loop för att beräkna vissa data som sedan ska renderas i en användargränssnittskontroll bör du överväga att köra loopen i en aktivitetsinstans i stället för direkt i en UI-händelsehanterare. Först när kärnberäkningen har slutförts bör du sedan konvertera UI-uppdateringen tillbaka till användargränssnittstråden.

Om du kör parallella loopar i användargränssnittstråden bör du undvika att uppdatera användargränssnittskontrollerna inifrån loopen. Försök att uppdatera användargränssnittskontroller inifrån en parallell loop som körs i användargränssnittstråden kan leda till tillståndsskada, undantag, fördröjda uppdateringar och till och med dödlägen, beroende på hur UI-uppdateringen anropas. I följande exempel blockerar den parallella loopen användargränssnittstråden som den körs på tills alla iterationer har slutförts. Men om en iteration av loopen körs på en bakgrundstråd (vilket For kan göra det), gör anropet till Invoke att ett meddelande skickas till användargränssnittstråden och blockerar väntar på att meddelandet ska bearbetas. Eftersom användargränssnittstråden blockeras när meddelandet körs Forkan det aldrig bearbetas och UI-trådens dödlägen.

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

I följande exempel visas hur du undviker dödläget genom att köra loopen i en aktivitetsinstans. Användargränssnittstråden blockeras inte av loopen och meddelandet kan bearbetas.

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

Se även