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 können Parallel.For und Parallel.ForEach erhebliche Leistungssteigerungen gegenüber gewöhnlichen sequenziellen Schleifen bieten. Die Parallelisierung der Schleife erhöht jedoch die Komplexität des Vorgangs, was Probleme nach sich ziehen kann, die in sequenziellem Code weniger häufig oder gar nicht vorkommen. In diesem Thema werden einige Praktiken aufgelistet, die beim Schreiben paralleler Schleifen zu vermeiden sind.
Gehen Sie nicht davon aus, dass eine parallele Ausführung immer schneller ist.
In bestimmten Fällen kann eine parallele Schleife langsamer als ihr sequenzielles Äquivalent ausgeführt werden. Eine Faustregel besagt, dass die Geschwindigkeit von parallelen Schleifen mit wenigen Iterationen und schnellen Benutzerdelegaten wahrscheinlich kaum zunimmt. Da jedoch viele Faktoren Einfluss auf die Leistung haben, wird empfohlen, immer die tatsächlichen Ergebnisse zu messen.
Vermeiden Sie es, in gemeinsam genutzte Speicherpositionen zu schreiben.
Bei sequenziellem Code wird regelmäßig aus statischen Variablen oder Klassenfeldern gelesen bzw. in diese geschrieben. Wenn jedoch mehrere Threads gleichzeitig auf diese Variablen zugreifen, besteht ein hohes Potenzial für Race Conditions. Sie können den Zugriff auf die Variable mithilfe von Sperren zwar synchronisieren, die Synchronisierung geht jedoch zu Lasten der Leistung. Es empfiehlt sich daher, den Zugriff auf den gemeinsamen Zustand in einer parallelen Schleife zu vermeiden oder zumindest so weit wie möglich einzuschränken. Der beste Weg, dies zu erreichen, ist die Verwendung der Überladungen von Parallel.For und Parallel.ForEach, die eine System.Threading.ThreadLocal<T>-Variable nutzen, um während der Schleifenausführung den threadlokalen Zustand zu speichern. Weitere Informationen finden Sie unter Vorgehensweise: Schreiben einer Parallel.For-Schleife mit threadlokalen Variablen und Vorgehensweise: Schreiben einer Parallel.ForEach-Schleife mit partitionslokalen Variablen.
Vermeiden Sie eine zu starke Parallelisierung.
Durch die Verwendung paralleler Schleifen tragen Sie die Mehrkosten für das Partitionieren der Quellsammlung und das Synchronisieren der Arbeitsthreads. Die Vorteile der Parallelisierung werden zudem durch die Anzahl der Prozessoren auf dem Computer beschränkt. Die Ausführung von mehreren rechnergebundenen Threads auf nur einem Prozessor ermöglicht keine Geschwindigkeitssteigerung. Achten Sie daher darauf, dass Sie eine Schleife nicht übermäßig parallelisieren.
Überparallelisierung tritt am häufigsten in geschachtelten Schleifen auf. In den meisten Fällen sollte idealerweise nur die äußere Schleife parallelisiert werden, sofern nicht eine oder mehrere der folgenden Bedingungen erfüllt sind:
Die innere Schleife ist bekanntermaßen sehr lang.
Sie führen für jede Bestellung eine umfangreiche Berechnung aus. (Der im Beispiel gezeigte Vorgang ist nicht sehr rechenintensiv.)
Das Zielsystem ist dafür bekannt, genügend Prozessoren zu haben, um die parallelisierte Verarbeitung zu bewältigen, die die Anzahl der Threads erzeugen wird.
In allen diesen Fällen empfiehlt es sich, die optimale Abfrageform mithilfe von Tests und Messungen zu ermitteln.
Vermeiden Sie den Aufruf nicht threadsicherer Methoden.
Das Schreiben in nicht threadsichere Instanzmethoden von einer parallelen Schleife aus kann zu Datenbeschädigungen führen, die im Programm möglicherweise unerkannt bleiben. Dies kann auch zu Ausnahmen führen. Im folgenden Beispiel würden mehrere Threads gleichzeitig versuchen, die FileStream.WriteByte-Methode aufzurufen, was von der Klasse nicht unterstützt wird.
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)))
Beschränken Sie Aufrufe auf threadsichere Methoden.
Die meisten statischen Methoden in .NET sind threadsicher und können von mehreren Threads gleichzeitig aufgerufen werden. Die damit verbundene Synchronisierung kann jedoch auch in diesen Fällen 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, sollten Sie sie nicht in parallelen Schleifen verwenden, es sei denn, es ist unbedingt erforderlich.
Beachten Sie Thread-Affinitätsprobleme.
Einige Technologien, z. B. COM-Interoperabilität für STA-Komponenten (Singlethread-Apartment), Windows Forms und Windows Presentation Foundation (WPF), erzeugen Threadaffinitätseinschränkungen, aufgrund derer Code in einem bestimmten Thread ausgeführt werden muss. Beispielsweise kann sowohl in Windows Forms als auch in WPF nur in einem Thread auf ein Steuerelement zugegriffen werden, in dem es erstellt wurde. Dies bedeutet beispielsweise, dass Sie kein Listensteuerelement von einer parallelen Schleife aktualisieren können, außer wenn Sie den Threadplaner konfigurieren, um die Arbeit nur im UI-Thread zu planen. Weitere Informationen finden Sie unter Angeben eines Synchronisierungskontexts.
Seien Sie vorsichtig, wenn Sie in einem Delegat warten, das von Parallel.Invoke aufgerufen wird.
Unter bestimmten Umständen wird ein Task von der Task Parallel Library inline ausgeführt, was bedeutet, dass der Task im aktuell ausgeführten Thread läuft. (Weitere Informationen finden Sie unter Task-Planer.) Diese Leistungsoptimierung kann in bestimmten Fällen zu einem Deadlock führen. Beispiel: Bei zwei Tasks wird möglicherweise der gleiche Delegatcode ausgeführt, der signalisiert, wenn ein Ereignis auftritt und anschließend auf die Signalisierung des anderen Tasks wartet. Wenn der zweite Task im gleichen Thread wie der erste Task inline ausgeführt wird und der erste Task in einen Wartezustand versetzt wird, kann der zweite Task das Ereignis niemals signalisieren. Um dies zu vermeiden, können Sie für den Wartevorgang ein Timeout angeben oder explizite Threadkonstruktoren verwenden, um sicherzustellen, dass ein Task nicht den anderen blockieren kann.
Gehen Sie nicht davon aus, dass Iterationen von „ForEach“, „For“ und „ForAll“ immer parallel ausgeführt werden.
Beachten Sie unbedingt, dass einzelne Iterationen in einer For-, ForEach- oder ForAll-Schleife parallel ausgeführt werden können, jedoch nicht zwangsläufig müssen. Schreiben Sie daher nach Möglichkeit keinen Code, dessen Korrektheit von der parallelen Ausführung von Iterationen oder der Ausführung von Iterationen in einer bestimmten Reihenfolge abhängig ist. Beim folgenden Code ist z. B. ein Deadlock wahrscheinlich:
ManualResetEventSlim mre = new ManualResetEventSlim();
Enumerable.Range(0, Environment.ProcessorCount * 100)
.AsParallel()
.ForAll((j) =>
{
if (j == Environment.ProcessorCount)
{
Console.WriteLine($"Set on {Thread.CurrentThread.ManagedThreadId} with value of {j}");
mre.Set();
}
else
{
Console.WriteLine($"Waiting on {Thread.CurrentThread.ManagedThreadId} with value of {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
In diesem Beispiel wird durch eine Iteration ein Ereignis festgelegt, und alle anderen Iterationen warten auf das Ereignis. Keine der wartenden Iterationen kann abgeschlossen werden, bevor die ereignissetzende Iteration 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 ereignisauslösende Iteration wird nicht ausgeführt, und die wartenden Iterationen werden niemals aufwachen.
Insbesondere sollte eine Iteration einer parallelen Schleife nie auf den Fortschritt einer anderen Iteration der Schleife warten. Wenn die parallele Schleife entscheidet, die Iterationen sequenziell, jedoch in der entgegengesetzten Reihenfolge zu planen, führt das zu einer Blockierung.
Vermeiden Sie die Ausführung paralleler Schleifen im Benutzeroberflächenthread.
Es ist wichtig, die Reaktionsfähigkeit der Benutzeroberfläche der Anwendung zu erhalten. Wenn ein Vorgang genug Arbeit enthält, um Parallelisierung zu garantieren, darf der Vorgang wahrscheinlich nicht im UI-Thread ausgeführt werden. Stattdessen sollte dieser Vorgang ausgelagert werden, um in einem Hintergrundthread ausgeführt zu werden. Wenn Sie z.B. eine parallele Schleife verwenden möchten, um einige Daten zu berechnen, die dann in ein UI-Steuerelement gerendert werden sollen, führen Sie die Schleife ggf. innerhalb einer Aufgabeninstanz und nicht direkt in einem UI-Ereignishandler aus. Erst nach der Kernberechnung können Sie die Aktualisierung der Benutzeroberfläche zurück zum UI-Thread marshallen.
Falls Sie parallele Schleifen im UI-Thread ausführen, vermeiden Sie es, UI-Elemente innerhalb der Schleife zu aktualisieren. Der Versuch, UI-Steuerelemente innerhalb einer parallelen Schleife zu aktualisieren, die im UI-Thread ausgeführt wird, kann zu Zustandsbeschädigung, Ausnahmen, verzögerten Updates und sogar Deadlocks führen, und zwar abhängig davon, wie das Update der Benutzeroberfläche aufgerufen wird. Im folgenden Beispiel blockiert die parallele Schleife den UI-Thread, in dem sie ausgeführt wird, bis alle Iterationen abgeschlossen wurden. Wenn eine Iteration der Schleife jedoch in einem Hintergrundthread ausgeführt wird (möglicherweise wie bei For), verursacht der Aufruf von „Invoke“ die Übermittlung einer Meldung an den UI-Thread, und er blockiert das Warten auf die Verarbeitung der Nachricht. Da der UI-Thread, in dem For ausgeführt wird, blockiert ist, kann die Nachricht nie verarbeitet werden, und im UI-Thread kommt es zu einem Deadlock.
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
Im folgenden Beispiel wird gezeigt, wie der Deadlock vermieden wird, indem die Schleife in einer Aufgabeninstanz ausgeführt wird. Der UI-Thread wird nicht von der Schleife blockiert, und die Meldung kann verarbeitet werden.
private void button2_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
Siehe auch
- Parallele Programmierung
- Potential Pitfalls with PLINQ (Potenzielle Fehler bei PLINQ)
- Patterns for Parallel Programming: Understanding and Applying Parallel Patterns with the .NET Framework 4 (Muster für die parallele Programmierung: Verstehen und Anwenden von parallelen Mustern mit .NET Framework 4)