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.
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.Orders
erzeugt 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.
PLINQ führt eine Abfrage zwar in mehreren Threads aus, doch wenn Sie die Ergebnisse in einer foreach
-Schleife (For Each
in Visual Basic) verarbeiten, müssen die Ergebnisse der Abfrage wieder in einem Thread zusammengeführt werden, und der Enumerator muss seriell darauf zugreifen. 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 sie erstellt wurde. Wenn Sie versuchen, in einer PLINQ-Abfrage auf den Freigabezustand eines Windows Forms-Steuerelements zuzugreifen, wird eine Ausnahme ausgelöst, wenn Sie den Debugger ausführen. (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.