Dela via


Potentiella fallgropar med PLINQ

I många fall kan PLINQ ge betydande prestandaförbättringar jämfört med sekventiella LINQ till objektfrågor. Arbetet med att parallellisera frågekörningen 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 PLINQ-frågor.

Anta inte att parallellen alltid är snabbare

Parallellisering gör ibland att en PLINQ-fråga körs långsammare än dess LINQ till motsvarande objekt. Den grundläggande tumregeln är att förfrågningar som har få källelement och snabba användardelegater sannolikt inte kommer att gå mycket snabbare. Eftersom många faktorer är inblandade i prestanda rekommenderar vi dock att du mäter faktiska resultat innan du bestämmer dig för om du vill använda PLINQ. För mer information, se Att förstå hastighetsökning i PLINQ.

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 PLINQ-fråga så mycket som möjligt.

Undvik överparallellisering

Genom att använda AsParallel-metoden ådrar du dig omkostnader för partitionering av källsamlingen och synkronisering av arbetstrådarna. Fördelarna med parallellisering begränsas ytterligare av antalet processorer på datorn. Det finns ingen hastighetsökning att få genom att köra flera trådar som är beräkningsbundna på bara en processor. Därför måste du vara noga med att inte överparallellisera en fråga.

Det vanligaste scenariot där överparallellisering kan ske är i kapslade frågor, enligt följande kodfragment.

var q = from cust in customers.AsParallel()
        from order in cust.Orders.AsParallel()
        where order.OrderDate > date
        select new { cust, order };
Dim q = From cust In customers.AsParallel()
        From order In cust.Orders.AsParallel()
        Where order.OrderDate > aDate
        Select New With {cust, order}

I det här fallet är det bäst att parallellisera endast den yttre datakällan (kunder) om inte ett eller flera av följande villkor gäller:

  • Den inre datakällan (cust.Orders) ä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 att parallellisera frågan på cust.Orders.

I samtliga fall är det bästa sättet att fastställa den optimala frågeformen att testa och mäta. Mer information finns i Så här mäter du PLINQ-frågeprestanda.

Undvik anrop till icke-trådsäkra metoder

Att skriva till icke-trådsäkra instansmetoder från en PLINQ-fråga kan leda till dataskada som kanske inte identifieras i programmet. Det kan också leda till undantag. I följande exempel försöker flera trådar anropa FileStream.Write metoden samtidigt, vilket inte stöds av klassen.

Dim fs As FileStream = File.OpenWrite(…)
a.AsParallel().Where(...).OrderBy(...).Select(...).ForAll(Sub(x) fs.Write(x))
FileStream fs = File.OpenWrite(...);
a.AsParallel().Where(...).OrderBy(...).Select(...).ForAll(x => fs.Write(x));

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 försening i frågeförfrågan.

Anmärkning

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 PLINQ-frågor.

Undvik onödiga beställningsåtgärder

När PLINQ kör en fråga parallellt delar den upp källsekvensen i partitioner som kan köras samtidigt på flera trådar. Som standard är ordningen i vilken partitionerna bearbetas och resultatet levereras inte förutsägbart (förutom för operatorer som OrderBy). Du kan instruera PLINQ att bevara ordningen på alla källsekvenser, men detta har en negativ inverkan på prestandan. Det bästa sättet när det är möjligt är att strukturera frågor så att de inte förlitar sig på orderbevarande. Mer information, se Ordningens bevarande i PLINQ.

Föredra ForAll till ForEach när det är möjligt

Även om PLINQ kör en fråga på flera trådar, måste frågeresultatet sammanfogas tillbaka till en tråd och användas seriellt av uppräknaren om du använder resultatet i en foreach loop (For Each i Visual Basic). I vissa fall är detta oundvikligt; Men när det är möjligt använder du ForAll metoden för att aktivera varje tråd för att mata ut sina egna resultat, till exempel genom att skriva till en trådsäker samling, System.Collections.Concurrent.ConcurrentBag<T>till exempel .

Samma problem gäller för Parallel.ForEach. Med andra ord source.AsParallel().Where().ForAll(...) borde starkt föredras över Parallel.ForEach(source.AsParallel().Where(), ...).

Var medveten om problem med tråd-affinitet

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å. Om du försöker komma åt det delade tillståndet för en Windows Forms-kontroll i en PLINQ-fråga utlöses ett undantag om du kör i felsökningsprogrammet. (Den här inställningen kan stängas av.) Men om frågan används i användargränssnittstråden kan du komma åt kontrollen från loopen foreach som räknar upp frågeresultatet eftersom koden körs på bara en tråd.

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 Parallel.For, Parallel.ForEacheller 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 orsaka en deadlock:

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

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.

Se även