CodeGym /Kurse /C# SELF /Parallele Datenverarbeitung

Parallele Datenverarbeitung

C# SELF
Level 60 , Lektion 3
Verfügbar

1. Einführung

Heute schauen wir uns an, wie man große Datenmengen so schnell wie möglich verarbeitet, indem man alle verfügbaren CPU-Kerne deines Rechners (oder Servers) nutzt. Dafür sind die Klassen aus dem Namespace System.Threading.Tasks.Parallel nützlich, konkret die Methoden Parallel.For und Parallel.ForEach.

Was tun, wenn die Aufgabe rein CPU-bound ist?

Eine klassische for- oder foreach-Schleife verarbeitet Elemente nacheinander. Einfach und zuverlässig. Aber auf einem Mehrkernprozessor nutzt die Schleife nur einen Kern, während die anderen faul herumliegen. Warum nicht Teile des Arrays auf verschiedene Kerne verteilen und parallel verarbeiten?

Beispiel:


// Wir berechnen die Summe der Quadrate von 1 bis N
long sum = 0;
for (int i = 1; i <= 1_000_000; i++)
{
    sum += i * i;
}

Dieser Code ist einfach, läuft aber sequentiell. Was, wenn wir die Arbeit auf mehrere Kerne verteilen?

Meet the Family: Parallel.For und Parallel.ForEach

Was ist das?

  • Parallel.For — funktioniert wie eine normale for-Schleife, teilt die Arbeit aber in Teile und verteilt sie automatisch auf Threads, wobei alle verfügbaren Kerne genutzt werden.
  • Parallel.ForEach — verarbeitet eine Collection wie eine normale foreach, aber parallel.

Offizielle Dokumentation:

Warum ist das praktisch?

Du musst keine Threads manuell erstellen, starten und kontrollieren. Das Framework übernimmt die harte Arbeit für dich. Du schreibst Code, der einer normalen Schleife ähnelt, und der Parallelismus passiert automatisch unter der Haube.

2. Syntax: grundlegende Beispiele

Parallel.For


long total = 0;
Parallel.For(1, 1_000_001, i =>
{
    // Dieses Lambda kann gleichzeitig von verschiedenen Threads ausgeführt werden
    Interlocked.Add(ref total, i * i); // Damit es keine Race-Conditions gibt
});
Console.WriteLine($"Summe der Quadrate: {total}");

Achte darauf: die Variable total aktualisieren wir über Interlocked.Add — um Datenrennen zu vermeiden.

Parallel.ForEach


var numbers = Enumerable.Range(1, 10_000_000).ToArray();
long sum = 0;

Parallel.ForEach(numbers, num =>
{
    Interlocked.Add(ref sum, num * num); // Sichere Addition
});
Console.WriteLine($"Summe der Quadrate: {sum}");

Blick unter die Haube (visuelle Darstellung)


+-------------------+
|Collection/Bereich |
+---------+---------+
          |
          v
  +----------------------+
  |   Parallel.ForEach   |
  +----------+-----------+
             |
        +----+----+----+----+
        |         |         |
        v         v         v
  Task #1    Task #2    Task #3   ... (verfügbare Kerne)
        |         |         |
     +--+----+  +--+-----+  +--+-----+
     |Verarbeitung|  |Verarbeitung|  |Verarbeitung|
     +-------+  +--------+  +--------+
        \         |         /
         +--------+--------+
                  |
                  v
             Ergebnis

3. Analyse großer Dateien (CPU-bound Verarbeitung)

Angenommen, wir haben eine Textdatei mit zehntausenden Zeilen — z.B. jede Zeile enthält eine Zahl. Wir müssen die Datei lesen, jede Zahl quadrieren und die Summe der Quadrate berechnen.

Synchrones Beispiel


string[] lines = File.ReadAllLines("numbers.txt");
long sum = 0;

foreach (var line in lines)
{
    if (long.TryParse(line, out long n))
    {
        sum += n * n;
    }
}
Console.WriteLine($"Summe der Quadrate: {sum}");

Parallele Version mit Parallel.For


string[] lines = File.ReadAllLines("numbers.txt");
long sum = 0;

Parallel.For(0, lines.Length, i =>
{
    if (long.TryParse(lines[i], out long n))
    {
        Interlocked.Add(ref sum, n * n);
    }
});
Console.WriteLine($"Summe der Quadrate: {sum}");

Was hat sich geändert: wir haben die normale Schleife durch eine parallele ersetzt, und sum erhöhen wir jetzt über Interlocked.Add — um Konflikte zwischen Threads zu vermeiden.

4. Was passiert unter der Haube?

Wenn du Parallel.For oder Parallel.ForEach aufrufst, teilt .NET automatisch deine Arbeit in Fragmente und verteilt sie auf verfügbare Prozessorkerne, indem der ThreadPool verwendet wird. Jedes Fragment wird unabhängig in seinem eigenen Thread verarbeitet.

Vorteil: bei 4 Kernen kann die Arbeit fast 4-mal schneller laufen (vorausgesetzt, die Aufgabe hängt nicht von externen Ressourcen ab und stößt nicht an andere Limits wie Speicher oder Festplatten-Leseleistung).

Vergleich der Laufzeiten


var numbers = Enumerable.Range(1, 100_000_000).ToArray();
long sumSync = 0;
var sw = System.Diagnostics.Stopwatch.StartNew();

foreach (var n in numbers)
    sumSync += n * n;

sw.Stop();
Console.WriteLine($"Sync: {sw.ElapsedMilliseconds} ms, summe: {sumSync}");

long sumParallel = 0;
sw.Restart();

Parallel.ForEach(numbers, n =>
    Interlocked.Add(ref sumParallel, n * n)
);

sw.Stop();
Console.WriteLine($"Parallel: {sw.ElapsedMilliseconds} ms, summe: {sumParallel}");

Probier es selbst aus! Auf einem starken Rechner kann der Speedup deutlich sein, aber das hängt von der Aufgabe und den Bottlenecks ab.

5. Nützliche Feinheiten

Steuerung des Parallelitätsgrads

Manchmal ist es sinnvoll, die Anzahl der verwendeten Threads zu begrenzen (z.B. um das System nicht zu überlasten). Dafür benutzt du MaxDegreeOfParallelism:


using System.Threading.Tasks;
long sum = 0;
var options = new ParallelOptions { 
    MaxDegreeOfParallelism = 2 
};
Parallel.For(0, 100, options, i =>
{
    Interlocked.Add(ref sum, i * i);
});
Console.WriteLine($"Summe der Quadrate: {sum}");

Wann das nützlich ist: wenn du weißt, dass ein Teil der Berechnungen stark die Festplatte belastet und nicht die CPU — dann setze weniger Threads und messe die Auswirkung auf die Performance.

Wann man parallele Schleifen verwenden sollte

Normale for Parallel.For/Parallel.ForEach
Prozessoren Nutzen einen Kern Nutzen alle Kerne
Reihenfolge Garantiert Nicht garantiert
Geschwindigkeit In der Regel langsamer Oft deutlich schneller
Einfachheit Sehr einfach Erfordert Beachtung von Thread-Safety
Beste Anwendung Kleine Datenmengen, I/O-bound Große Datenmengen, CPU-bound

Erweiterung: was kann Parallel sonst noch?

Parallel.Invoke() — startet mehrere unabhängige Methoden gleichzeitig:


static void DoTask1() => Console.WriteLine("Aufgabe 1 erledigt");
static void DoTask2() => Console.WriteLine("Aufgabe 2 erledigt");
static void DoTask3() => Console.WriteLine("Aufgabe 3 erledigt");

Parallel.Invoke(
    () => DoTask1(),
    () => DoTask2(),
    () => DoTask3()
);

Jede Methode wird nach Möglichkeit auf einem eigenen Kern ausgeführt.

Anwendungen im echten Leben

  • Bildverarbeitung: parallele Verarbeitung verschiedener Blöcke (z.B. Anwendung eines Filters).
  • Berechnungen über Arrays: Finanzberechnungen, Simulationen (Bewertung eines Portfolios über Szenarien).
  • Arbeiten mit großen Logfiles: Suche und Aggregation über mehrere Kerne.
  • Machine Learning: Aufteilen in unabhängige Tasks (Datenbatches, Feature Engineering).

Und natürlich kannst du im Vorstellungsgespräch nicht nur erklären, was parallele Schleifen sind, sondern auch ehrlich ihre Vor- und Nachteile diskutieren.

6. Typische Fehler bei der Arbeit mit Parallel.For und Parallel.ForEach

Fehler Nr.1: Ignorieren von Race-Conditions.
Das Aktualisieren einer gemeinsamen Variablen ohne Interlocked oder lock führt zu falschen Ergebnissen wegen gleichzeitigen Zugriffs durch mehrere Threads.

Fehler Nr.2: Einsatz bei I/O-bound Aufgaben.
Parallele Schleifen beschleunigen keine Aufgaben, die von Festplatte oder Netzwerk abhängen, und können sie wegen Overheads sogar verlangsamen.

Fehler Nr.3: Annahme über Ausführungsreihenfolge.
Parallele Schleifen garantieren nicht die Reihenfolge der Verarbeitung, was die Logik brechen kann, wenn sie von Sequenz abhängt.

Fehler Nr.4: Ignorieren von Seiteneffekten.
Das Ändern von gemeinsamem Zustand (z.B. Collections) in parallelen Schleifen kann zu Fehlern führen, wenn nicht threadsichere Strukturen verwendet werden.

Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION