Potenzielle Fehler bei Daten- und Aufgabenparallelität

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 sind bestimmte Fehlerquellen aufgeführt, die beim Schreiben von parallelen Schleifen vermieden werden sollten.

Gehen Sie nicht davon aus, dass eine parallele Ausführung immer schneller ist.

In bestimmten Fällen kann eine parallele Schleife langsamer als deren sequenzielle Entsprechung 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 eine hohe Wahrscheinlichkeit für Racebedingungen. 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 Freigabezustand in einer parallelen Schleife zu vermeiden oder so weit wie möglich einzuschränken. Dies geschieht am besten durch Verwendung der Überladungen von Parallel.For und Parallel.ForEach, die eine System.Threading.ThreadLocal<T>-Variable zum Speichern des threadlokalen Zustands während der Schleifenausführung verwenden. 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.

Mithilfe von parallelen Schleifen übernehmen Sie die Mehrkosten für das Partitionieren der Quellauflistung 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 zu stark parallelisieren.

Eine zu starke Parallelisierung tritt vor allem in geschachtelten Schleifen auf, wie im folgenden Ausschnitt gezeigt wird. 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 verfügt über genug Prozessoren für die Verarbeitung der Anzahl von Threads, die durch die Parallelisierung der Abfrage von cust.Orders erzeugt werden.

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. Diese Methode wird jedoch nur in den Dokumentationsbeispielen zu Demonstrationszwecken verwendet. Nutzen Sie sie nur in parallelen Schleifen, wenn dies erforderlich ist.

Beachten Sie Threadaffinitä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 Delegaten warten, die von Parallel.Invoke aufgerufen werden.

Unter bestimmten Umständen wird ein Task von der Task Parallel Library inline ausgeführt, d.h. die Ausführung erfolgt im derzeit ausgeführten Thread. (Weitere Informationen finden Sie unter TaskScheduler-Klasse.) Diese Leistungsoptimierung kann in bestimmten Fällen einen Deadlock zur Folge haben. 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 parallel ausgeführt werden 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 {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
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. Die wartenden Iterationen können erst nach Abschluss der ereignisauslösenden Iteration abgeschlossen werden. 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 niemals ausgeführt, und die wartenden Iterationen werden zu keinem Zeitpunkt aktiviert.

Insbesondere sollte eine Iteration einer parallelen Schleife nie auf den Fortschritt einer anderen Iteration der Schleife warten. Wenn von der parallelen Schleife entschieden wird, die Iterationen sequenziell zu planen, jedoch in der entgegengesetzten Reihenfolge, tritt ein Deadlock auf.

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 der Vorgang abgeladen werden, um eine Ausführung als Hintergrundthread zu ermöglichen. 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.

Wenn Sie parallele Schleifen im UI-Thread ausführen, aktualisieren Sie innerhalb der Schleife keine Benutzeroberflächenelemente. 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 button1_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