Neben der Aufgabenparallelität, hat die TPL auch die Datenparallelität eingeführt. Damit kannst du mühelos eine for
oder foreach
Schleife parallelisieren. So ist wenigstens die Idee. Auch wenn die Entwickler der TPL einen brillianten Job gemacht haben, hast du es immer noch mit nebenläufiger Programmierung zutun. Daher solltest du dir bei jeder Schleife überlegen welche Auswirkungen eine Parallelisierung haben kann.
Nehmen wir einmal ein typisches Beispiel:
static void SummenformelSequentiell()
{
//Alle Zahlen von 1 - 10000000 generieren
var zahlen = Enumerable.Range(1, 10000000);
long summe = 0;
foreach(var zahl in zahlen)
{
summe += zahl;
}
// Resultat ist: 50000005000000
Console.WriteLine("Die Summe aller Zahlen von 1-10000000 ist: {0}", summe);
}
Mit der ForEach
Methode der Klasse Parallel
aus dem Namensraum System.Threading.Tasks
, kannst du diese Ausführung eigentlich ganz einfach parallelisieren. Wenn da nicht die lokale Variable summe
wäre. Denn was passiert, wenn du die Schleife wie folgt änderst? Parallel.ForEach(zahlen, zahl => summe += zahl);
Richtig, das hängt absolut von deinem Rechner ab. Allerdings wirst du nur in den wenigsten Fällen das richtige Ergebnis bekommen. Die Idee ist, dass Parallel.ForEach
den Inhalt des übergebenen IEnumerable<T>
partitioniert und versucht mit mehreren Threads parallel abzuarbeiten. Verwendest du also eine lokale Variable, ein Klassenattribut, … auf das nicht thread-safe zugegriffen werden kann, wird dies von den Threads mehrfach im inkonsistenten Zustand hinterlassen, weil sie parallel darauf zugreifen.
Eine mögliche Lösung des Problems ist lock { summe += zahl }
zu verwenden. Damit erhälst du zwar das richtige Ergebnis, aber in vielen Fällen blockieren sich die Threads soviel, dass es keinen Gewinn bei der Ausführungszeit gibt. Der bessere Weg heißt thread-local data.
Sowohl Parallel.ForEach
wie auch Parallel.For
erlauben es, dass du eine für den Thread lokale Variable definierst. Jedesmal, wenn ein Bereich deiner Daten, an einen Thread zur Abarbeitung gegeben wird, wird diese Variable initialisiert und steht solange für den Thread bereit, wie er Iterationen auf den Daten ausführt. Ist der Thread dann fertig, kannst du entscheiden wie der Wert der lokalen Variablen verwendet werden soll. Zurück zum Beispiel von eben:
Parallel.ForEach<int, long>
(
zahlen,
() => 0L,
(zahl, loopState, zwischenSumme) =>
{
zwischenSumme += zahl; //Bilde lokale zwischenSumme - pro Thread
return zwischenSumme;
},
zwischenSumme => Interlocked.Add(ref summe, zwischenSumme)
);
Damit ist die ursprünglich einfache foreach
Schleife schon wesentlich komplexer geworden. Was aber bedeutet es nun im Detail? Das ist immer noch nicht so kompliziert.
Der erste Parameter von Parallel.ForEach
sind die Daten auf denen gearbeitet werden soll. Als nächstes musst du dem Thread mitteilen, wie seine lokale Variable initialisiert werden soll. Dies geht einfach über ein Func<TLocal>
delegate. Dann folgt der eigentliche Schleifenrumpf. Dort taucht zum erstmal die thread-lokale Variable auf. Hier heißt sie zwischenSumme
. Im letzten Parameter definierst du was mit der thread-lokalen Variable geschehen soll, wenn der Thread fertig ist mit der Berechnung.
Obwohl auch in diesem Szenario für die Bildung der Gesamtsumme ein lock
bzw. in diesem Beispiel Interlocked.Add
verwendet wird, ist die Ausführung wesentlich schneller. Denn es wird nicht bei jeder Iteration zwischen den Threads synchronisiert, sondern nur noch, wenn der jeweilige Thread fertig ist. Auch dann ist nicht mal sicher, dass die Threads sich blockieren.
Jetzt erstmal viel Spaß mit dem Synchronisieren von parallelen Schleifen
Jan
Merke
- Mit
Parallel.ForEach
undParallel.For
kannst du eine Parallelität beim bearbeiten deiner Daten erreichen. - Auch wenn du dabei nicht direkt mit
Thread
oderTask
arbeitest, musst du trotzdem die üblichen Fallen der nebenläufigen Programmierung beachten. - Eine thread-lokale Variable erspart dir das ständige blockieren von Threads bei der Synchronisation.
- Wenn du eine thread-lokale Variable verwendest, ist diese nur innerhalb des Kontexts von
Parallel.ForEach
bzw.Parallel.For
verfügbar. - Neben der Verwendung im Schleifenrumpf, musst du thread-lokale Variablen initialisieren und solltest definieren, wie das Resultat verwendet wird, wenn der Thread fertig ist.
Lernquiz
Verwende folgende Fragen, um das Gelernte von heute zu festigen:
- Welche Möglichkeiten gibt es zur Synchronisation von Threads bei der Verwendung von
Parallel.ForEach
undParallel.For
? - Wie kannst du eine thread-lokale Variable vom Typ
float
auf den Wert3.14
initialisieren? - Wie kannst du den Wert einer thread-lokalen Variable verwenden, wenn der Thread fertig ist?
Am besten schaust du dir morgen und dann nochmal in ein paar Tagen die vorherigen Fragen an und beantwortest sie, ohne den Text vorher gelesen zu haben.
Weitere Informationen
- Den kompletten Quelltext zum heutigen Lernmoment findest du hier.
- Eine detailierte Einführung auf Englisch gibt es bei BlackWasp.
- Die Beschreibung der verwendeten Methode auf MSDN.
- Eine umfangreiche Einführung in das Thema Datenparallelität findest du ebenfalls auf MSDN