Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
In vielen Fällen kann PLINQ erhebliche Leistungsverbesserungen gegenüber sequenziellen LINQ to Objects-Abfragen bieten. Die Parallelisierung der Abfrageausführung führt jedoch zu Einer Komplexität, die zu Problemen führen kann, die im sequenziellen Code nicht so häufig sind oder überhaupt nicht gefunden werden. In diesem Thema werden einige Methoden aufgeführt, die beim Schreiben von PLINQ-Abfragen vermieden werden sollen.
Gehen Sie nicht davon aus, dass parallel immer schneller ist
Parallelisierung führt manchmal dazu, dass eine PLINQ-Abfrage langsamer ausgeführt wird als die LINQ to Objects-Entsprechung. Eine Faustregel besagt, dass die Geschwindigkeit von Abfragen mit wenigen Quellelementen und schnellen Benutzerdelegaten wahrscheinlich kaum zunimmt. Da jedoch viele Faktoren an der Leistung beteiligt sind, empfehlen wir, die tatsächlichen Ergebnisse zu messen, bevor Sie entscheiden, ob PLINQ verwendet werden soll. Weitere Informationen finden Sie unter Understanding Speedup in PLINQ.
Vermeiden des Schreibens an freigegebene Speicherorte
Im sequenziellen Code ist es nicht ungewöhnlich, aus statischen Variablen oder Klassenfeldern zu lesen oder zu schreiben. Wenn jedoch mehrere Threads gleichzeitig auf solche Variablen zugreifen, besteht ein großes Potenzial für Rennbedingungen. Obwohl Sie Sperren verwenden können, um den Zugriff auf die Variable zu synchronisieren, kann die Synchronisierungskosten die Leistung beeinträchtigen. Daher wird empfohlen, den Zugriff auf den freigegebenen Zustand in einer PLINQ-Abfrage so weit wie möglich zu vermeiden oder zumindest einzuschränken.
Beispiel: Racebedingung mit freigegebenem Speicher
Im folgenden Beispiel wird eine Race Condition veranschaulicht, die auftritt, wenn mehrere Threads in eine gemeinsam genutzte Variable schreiben. Auf die Variable total wird gleichzeitig von mehreren Threads ohne Synchronisierung zugegriffen und geändert, was zu unvorhersehbaren Ergebnissen führt:
static void DemonstrateRaceCondition()
{
int total = 0;
var numbers = Enumerable.Range(0, 10000);
// UNSAFE: Multiple threads writing to shared variable
numbers.AsParallel().ForAll(n => total += n);
Console.WriteLine($"Total (with race condition): {total}");
// Expected: 49,995,000 but result is unpredictable due to race condition
}
Shared Sub DemonstrateRaceCondition()
Dim total As Integer = 0
Dim numbers = Enumerable.Range(0, 10000)
' UNSAFE: Multiple threads writing to shared variable
numbers.AsParallel().ForAll(Sub(n) total += n)
Console.WriteLine($"Total (with race condition): {total}")
' Expected: 49,995,000 but result is unpredictable due to race condition
End Sub
In diesem Code ist der Vorgang total += n nicht atomar. Es umfasst das Lesen des aktuellen Werts von total, Hinzufügen nund Schreiben des Ergebnisses zurück in total. Wenn mehrere Threads diesen Vorgang gleichzeitig ausführen, können sie denselben Wert lesen, ihn in verschiedenen Threads hinzufügen und Ergebnisse zurückschreiben, die einander überschreiben. Dies führt dazu, dass einige Ergänzungen verloren gehen, was zu einem falschen Endergebnis führt.
Der richtige Ansatz besteht darin, threadsichere Vorgänge zu verwenden, die keinen gemeinsam genutzten änderbaren Zustand erfordern:
static void DemonstrateCorrectApproach()
{
var numbers = Enumerable.Range(0, 10000);
// SAFE: Use thread-safe aggregate operation
int total = numbers.AsParallel().Sum();
Console.WriteLine($"Total (correct): {total}");
// Result is always 49,995,000
}
Shared Sub DemonstrateCorrectApproach()
Dim numbers = Enumerable.Range(0, 10000)
' SAFE: Use thread-safe aggregate operation
Dim total As Integer = numbers.AsParallel().Sum()
Console.WriteLine($"Total (correct): {total}")
' Result is always 49,995,000
End Sub
Die Sum Methode verarbeitet die Parallelisierung intern in threadsicherer Weise und stellt die richtigen Ergebnisse sicher, ohne dass eine explizite Synchronisierung erforderlich ist. Andere sichere Ansätze umfassen die Verwendung von Aggregate für benutzerdefinierte Aggregationen oder das Sammeln von Ergebnissen in threadsicheren Sammlungen wie ConcurrentBag<T>.
Vermeiden von zu starker Parallelisierung
Durch die Verwendung der AsParallel Methode entstehen die Mehrkosten für die Partitionierung der Quellsammlung und die Synchronisierung der Worker-Threads. Die Vorteile der Parallelisierung werden durch die Anzahl der Prozessoren auf dem Computer weiter eingeschränkt. Es gibt keine Beschleunigung, die durch Ausführen mehrerer computegebundener Threads auf nur einem Prozessor gewonnen werden kann. Daher müssen Sie darauf achten, eine Abfrage nicht zu überparallelisieren.
Das häufigste Szenario, in dem Über-Parallelisierung auftreten kann, ist in geschachtelten Abfragen, wie im folgenden Codeausschnitt gezeigt.
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 diesem Fall empfiehlt es sich, nur die äußere Datenquelle (Kunden) zu parallelisieren, es sei denn, eine oder mehrere der folgenden Bedingungen gelten:
Von der internen Datenquelle (cust.Orders) ist bekannt, dass sie sehr groß ist.
Sie führen eine teure Berechnung für jede Bestellung durch. (Der im Beispiel gezeigte Vorgang ist nicht teuer.)
Es ist bekannt, dass das Zielsystem genügend Prozessoren besitzt, um die Anzahl der Threads zu verarbeiten, die durch die Parallelisierung der Abfrage auf
cust.Orderserzeugt werden.
In allen Fällen besteht die beste Möglichkeit, das optimale Abfrage-Shape zu ermitteln, darin, zu testen und zu messen. Weitere Informationen finden Sie unter How to: Measure PLINQ Query Performance.
Sicherstellen, dass keine nicht thread-sicheren Methoden aufgerufen werden
Das Schreiben in nicht threadsichere Instanzmethoden aus einer PLINQ-Abfrage kann zu Datenbeschädigungen führen, die möglicherweise in Ihrem Programm nicht erkannt werden. Sie kann auch zu Ausnahmen führen. Im folgenden Beispiel würden mehrere Threads versuchen, die FileStream.Write Methode gleichzeitig aufzurufen, die von der Klasse nicht unterstützt wird.
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));
Beschränken Sie Aufrufe auf threadsichere Methoden
Die meisten statischen Methoden in .NET sind threadsicher und können gleichzeitig aus mehreren Threads aufgerufen werden. Selbst in diesen Fällen kann die Synchronisierung jedoch zu einer erheblichen Verlangsamung der Abfrage führen.
Hinweis
Sie können dies testen, indem Sie in Ihre Abfragen Aufrufe von WriteLine einfügen. Obwohl diese Methode in den Dokumentationsbeispielen zu Demonstrationszwecken verwendet wird, verwenden Sie sie nicht in PLINQ-Abfragen.
Vermeiden sie unnötige Sortierungsvorgänge
Wenn PLINQ eine Abfrage parallel ausführt, teilt sie die Quellsequenz in Partitionen auf, die gleichzeitig auf mehreren Threads ausgeführt werden können. Standardmäßig ist die Reihenfolge, in der die Partitionen verarbeitet werden und die Ergebnisse übermittelt werden, nicht vorhersehbar (mit Ausnahme von Operatoren wie OrderBy). Sie können PLINQ anweisen, die Reihenfolge einer beliebigen Quellsequenz beizubehalten, dies hat jedoch negative Auswirkungen auf die Leistung. Die bewährte Methode besteht nach Möglichkeit darin, Abfragen so zu strukturieren, dass sie nicht auf die Erhaltung der Reihenfolge angewiesen sind. Weitere Informationen finden Sie unter "Order Preservation" in PLINQ.
Bevorzugen Sie ForAll anstelle von ForEach, wann immer es möglich ist.
Obwohl PLINQ eine Abfrage für mehrere Threads ausführt, müssen die Abfrageergebnisse, wenn diese in einer foreach-Schleife (For Each in Visual Basic) verwendet werden, in einem Thread zusammengeführt und vom Enumerator seriell aufgerufen werden. In einigen Fällen ist dies unvermeidbar; verwenden Sie jedoch nach Möglichkeit die ForAll-Methode, um jedem Thread die Ausgabe eigener Ergebnisse zu ermöglichen, z. B. durch das Schreiben in eine threadsichere Auflistung wie System.Collections.Concurrent.ConcurrentBag<T>.
Dasselbe Problem gilt für Parallel.ForEach. Mit anderen Worten, source.AsParallel().Where().ForAll(...) sollte stark bevorzugt werden gegenüber Parallel.ForEach(source.AsParallel().Where(), ...).
Beachten Sie Threadaffinitätsprobleme
Einige Technologien, z. B. COM-Interoperabilität für Single-Threaded Apartment(STA)-Komponenten, Windows Forms und Windows Presentation Foundation (WPF), erzwingen Threadaffinitätseinschränkungen, die die Ausführung von Code für einen bestimmten Thread erfordern. Beispielsweise kann in Windows Forms und WPF nur auf ein Steuerelement im Thread zugegriffen werden, auf den es erstellt wurde. Wenn Sie versuchen, auf den freigegebenen Zustand eines Windows Forms-Steuerelements in einer PLINQ-Abfrage zuzugreifen, wird eine Ausnahme ausgelöst, wenn Sie den Debugger verwenden. (Diese Einstellung kann deaktiviert werden.) Wenn Ihre Abfrage jedoch im UI-Thread verwendet wird, können Sie über die foreach Schleife, die die Abfrageergebnisse aufzählt, auf das Steuerelement zugreifen, da dieser Code nur in einem Thread ausgeführt wird.
Gehen Sie nicht davon aus, dass Iterationen von ForEach, For und ForAll immer parallel ausgeführt werden
Es ist wichtig zu beachten, dass einzelne Iterationen in einer Parallel.For-, Parallel.ForEach- oder ForAll-Schleife möglicherweise, aber nicht notwendigerweise, parallel ausgeführt werden. Daher sollten Sie vermeiden, code zu schreiben, der von der korrekten Ausführung von Iterationen oder von der Ausführung von Iterationen in einer bestimmten Reihenfolge abhängt.
Dieser Code wird z. B. wahrscheinlich eine Blockierung verursachen:
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 diesem Beispiel legt eine Iteration ein Ereignis fest, und alle anderen Iterationen warten auf das Ereignis. Keine der wartenden Iterationen kann abgeschlossen werden, bis die Iteration der Ereigniseinstellung abgeschlossen ist. Es ist jedoch möglich, dass die wartenden Iterationen alle Threads blockieren, die zur Ausführung der parallelen Schleife verwendet werden, bevor die ereignisauslösende Iteration überhaupt ausgeführt werden kann. Dies führt zu einem Deadlock – die Iteration der Ereigniseinstellung wird nie ausgeführt, und die wartenden Iterationen werden nie aufwachen.
Insbesondere sollte eine Iteration einer parallelen Schleife niemals auf eine andere Iteration der Schleife warten, um Fortschritte zu erzielen. Wenn die parallele Schleife entscheidet, die Iterationen sequenziell, aber in der entgegengesetzten Reihenfolge zu planen, tritt ein Deadlock auf.