Mogelijke valkuilen met PLINQ
In veel gevallen kan PLINQ aanzienlijke prestatieverbeteringen bieden ten opzichte van sequentiële LINQ naar objectenquery's. Het werk van het parallelliseren van de queryuitvoering 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 PLINQ-query's schrijft.
Ga er niet van uit dat parallel altijd sneller is
Parallellisatie zorgt er soms voor dat een PLINQ-query langzamer wordt uitgevoerd dan de LINQ naar objecten die gelijkwaardig zijn. De basisregel van duim is dat query's met weinig bronelementen en snelle gebruikersdelegenten waarschijnlijk veel sneller werken. Omdat er echter veel factoren bij de prestaties betrokken zijn, raden we u aan werkelijke resultaten te meten voordat u besluit of u PLINQ wilt gebruiken. Zie Inzicht in snelheid in PLINQ voor meer informatie.
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 wordt u aangeraden de toegang tot de gedeelde status in een PLINQ-query zoveel mogelijk te vermijden of te beperken.
Overparallellisatie voorkomen
Met behulp van de AsParallel
methode 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 query niet te parallelliseren.
Het meest voorkomende scenario waarin overparallellisatie kan optreden, bevindt zich in geneste query's, zoals wordt weergegeven in het volgende fragment.
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}
In dit geval kunt u het beste alleen de buitenste gegevensbron (klanten) parallelliseren, tenzij een of meer van de volgende voorwaarden van toepassing zijn:
De binnenste gegevensbron (cust. Orders) is bekend dat het erg lang is.
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.Orders
parallel te maken.
In alle gevallen kunt u de optimale queryshape het beste testen en meten. Zie Procedure: PLINQ-queryprestaties meten voor meer informatie.
Vermijd aanroepen naar niet-threadveilige methoden
Schrijven naar niet-threadveilige exemplaarmethoden van een PLINQ-query kan leiden tot beschadiging van gegevens die mogelijk of niet worden gedetecteerd in uw programma. Het kan ook leiden tot uitzonderingen. In het volgende voorbeeld proberen meerdere threads tegelijkertijd de FileStream.Write
methode aan te roepen, die niet wordt ondersteund door de klasse.
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));
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 gebruiken in PLINQ-query's.
Vermijd onnodige bestelbewerkingen
Wanneer PLINQ een query parallel uitvoert, wordt de bronreeks verdeeld in partities waarop gelijktijdig op meerdere threads kan worden uitgevoerd. Standaard is de volgorde waarin de partities worden verwerkt en de resultaten niet voorspelbaar zijn (met uitzondering van operators zoals OrderBy
). U kunt PLINQ instrueren om de volgorde van elke bronvolgorde te behouden, maar dit heeft een negatieve invloed op de prestaties. Waar mogelijk is het best om query's zodanig te structuren dat ze niet afhankelijk zijn van het behoud van de bestelling. Zie Orderbehoud in PLINQ voor meer informatie.
Geef De voorkeur aan ForAll aan ForEach wanneer het mogelijk is
Hoewel PLINQ een query uitvoert op meerdere threads, moet u, als u de resultaten in een foreach
lus (For Each
in Visual Basic) gebruikt, de queryresultaten weer worden samengevoegd in één thread en serially worden geopend door de enumerator. In sommige gevallen is dit onvermijdelijk; Gebruik echter, indien mogelijk, de ForAll
methode om elke thread in staat te stellen zijn eigen resultaten uit te voeren, bijvoorbeeld door te schrijven naar een thread-safe-verzameling, zoals System.Collections.Concurrent.ConcurrentBag<T>.
Hetzelfde probleem is van toepassing op Parallel.ForEach. Met andere woorden, source.AsParallel().Where().ForAll(...)
moet sterk worden voorkeur aan Parallel.ForEach(source.AsParallel().Where(), ...)
.
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. Als u probeert toegang te krijgen tot de gedeelde status van een Windows Forms-besturingselement in een PLINQ-query, wordt er een uitzondering gegenereerd als u in het foutopsporingsprogramma werkt. (Deze instelling kan worden uitgeschakeld.) Als uw query echter wordt gebruikt in de UI-thread, hebt u toegang tot het besturingselement vanuit de foreach
lus waarmee de queryresultaten worden opgesomd, omdat die code wordt uitgevoerd op slechts één thread.
Stel niet dat iteraties van ForEach, For en ForAll altijd parallel worden uitgevoerd
Het is belangrijk om in gedachten te houden dat afzonderlijke iteraties in een Parallel.For, Parallel.ForEachof ForAll lus kunnen worden uitgevoerd, 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:
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
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.