1. Nebenläufigkeit vs. Parallelität
Nebenläufigkeit: viel, aber nicht zwingend gleichzeitig
Nebenläufigkeit bedeutet, dass Ihre Anwendung mehrere Ausführungs-Threads hat. Jeder Thread ist wie eine eigene Handlungslinie: einer rechnet etwas, ein anderer wartet auf Benutzereingaben, ein dritter speichert Daten in eine Datei. In Java erstellen Sie Threads über die Klasse Thread, implementieren das Interface Runnable oder verwenden High-Level-Werkzeuge wie ExecutorService (dazu – in der nächsten Vorlesung).
ABER! Nebenläufigkeit garantiert nicht, dass Ihre Aufgaben wirklich gleichzeitig ausgeführt werden. Das hängt davon ab, wie viele Kerne Ihr Prozessor hat. Wenn es nur einen Kern gibt, „wechseln“ die Threads einfach schnell zwischen einander – so schnell, dass es dem Menschen so vorkommt, als passiere alles gleichzeitig. Tatsächlich führt der Prozessor immer nur einen Thread zu einem Zeitpunkt aus, die übrigen warten auf ihre Reihe.
Parallelität: wenn Aufgaben wirklich gleichzeitig laufen
Parallelität bedeutet, dass Ihr Code tatsächlich gleichzeitig auf mehreren Prozessorkernen ausgeführt wird. Wenn Sie einen modernen Rechner mit 4, 8, 16 Kernen haben – können Sie die Bearbeitung großer Aufgaben wirklich beschleunigen, indem Sie sie in unabhängige Teile zerlegen und auf die Kerne verteilen.
Zur Analogie: Nebenläufigkeit ist, wenn Sie einen Koch haben, der schnell zwischen Borschtsch kochen, Frikadellen braten und Salat schneiden wechselt. Parallelität ist, wenn Sie mehrere Köche gleichzeitig haben und jeder für sein eigenes Gericht zuständig ist.
Worin liegt der Unterschied in der Praxis?
Nebenläufigkeit steht für Bequemlichkeit und Reaktionsfähigkeit. Sie verwenden mehrere Threads, damit das Programm nicht „hängt“: ein Thread wartet auf das Netzwerk, ein anderer zeichnet die Benutzeroberfläche, ein dritter rechnet etwas. Alles wirkt parallel, aber muss nicht gleichzeitig laufen.
Parallelität steht für Geschwindigkeit. Hier führen wirklich mehrere Prozessorkerne unterschiedliche Teile der Aufgabe gleichzeitig aus, um schneller zum Ergebnis zu kommen.
Anders gesagt: Nebenläufigkeit hilft, die Arbeit zu organisieren, Parallelität – sie zu beschleunigen.
Wichtig:
Nebenläufigkeit ist immer dann nötig, wenn es Aufgaben gibt, die unabhängig voneinander erledigt werden können.
Parallelität ist nötig, wenn Sie Berechnungen beschleunigen wollen, indem die Arbeit wirklich auf Kerne verteilt wird.
Beispiel: Verarbeitung eines großen Arrays
Stellen wir uns vor, wir haben ein Array mit 10 Millionen Zahlen und wollen die Summe aller Elemente berechnen.
Sequentiell:
Ein Thread läuft durch das gesamte Array und berechnet die Summe. Einfach und zuverlässig, aber langsam.
Nebenläufig (aber auf einem Kern):
Sie teilen das Array in 4 Teile, erstellen 4 Threads, jeder berechnet seinen Teil. Wenn Sie jedoch nur einen Kern haben, arbeiten die Threads einfach nacheinander – es gibt keine Beschleunigung, und der Overhead durch Kontextwechsel kann das Programm sogar verlangsamen.
Parallel (auf mehreren Kernen):
Sie teilen das Array in 4 Teile, starten 4 Threads und jeder Thread läuft tatsächlich auf seinem eigenen Kern. Die Gesamtsumme wird aus 4 Teilen zusammengesetzt. Das ist wirklich schneller – besonders bei großen Datenmengen.
Dafür ist die sequentielle Verarbeitung eines Arrays sehr einfach umzusetzen, solche Programme haben Sie schon oft geschrieben:
// Beispiel: sequentielle Verarbeitung eines Arrays
int[] arr = new int[10_000_000];
// ... Array füllen ...
long sum = 0;
for (int x : arr) {
sum += x;
}
System.out.println(sum);
Die nebenläufigen und parallelen Versionen sind etwas komplexer; wir werden sie in den nächsten Vorlesungen mithilfe moderner Werkzeuge durchgehen.
2. Warum Parallelität nötig ist
Moderne Prozessoren gehen längst über einen einzelnen Kern hinaus. Selbst Ihr Smartphone hat höchstwahrscheinlich mindestens vier, und Desktop-Rechner sowie Server – acht, sechzehn, zweiunddreißig und mehr. Wenn eine Anwendung all diese Kerne nutzen kann, arbeitet sie um ein Vielfaches schneller.
Früher wuchs die Prozessorleistung durch die Erhöhung der Taktfrequenz – bis etwa Mitte der 2000er Jahre hat das tatsächlich funktioniert. Doch der Frequenzanstieg stieß an physikalische Grenzen, und dann begann eine neue Ära – Multiprozessor- und Mehrkernsysteme. Jetzt gewinnen die Programme, die Arbeit effektiv auf Kerne verteilen können.
Wo beschleunigt Parallelität wirklich?
- Verarbeitung großer Datenmengen: Log-Analyse, Statistik, Aggregation – alles, was in unabhängige Stücke zerlegt werden kann.
- Rendering, Bild- und Videoverarbeitung: jeder Pixel oder Abschnitt kann separat verarbeitet werden.
- Wissenschaftliches Rechnen, Simulation: mathematische Aufgaben, Simulationen, Trainieren von Modellen.
- Serveranwendungen: gleichzeitige Bedienung vieler Clients.
- Reaktive Anwendungen: wenn schnell auf viele Ereignisse reagiert werden muss, ohne den Haupt-Thread zu blockieren.
Wann hilft Parallelität nicht?
- Wenn die Aufgabe klein ist, kann der Overhead für das Starten der Parallelität größer sein als der Gewinn.
- Wenn sich die Aufgabe nicht in unabhängige Teile zerlegen lässt (zum Beispiel wenn jeder Schritt vom vorherigen abhängt).
- Wenn es viele gemeinsame Ressourcen gibt (z. B. dieselbe Datei) und Threads sich gegenseitig behindern.
3. Typische Aufgaben für Parallelität
Schauen wir uns an, welche Aufgaben am häufigsten auf Kerne „verteilt“ werden.
Massive Berechnungen
- Summieren, Maximum/Minimum finden, Statistiken über ein großes Array berechnen.
- Beispiel: den Durchschnittswert der Temperatur über eine Million Sensoren berechnen.
Verarbeitung von Collections
- Filtern, Sortieren, Transformieren großer Listen (z. B. die Verarbeitung von Bestellungen eines Online-Shops).
- Beispiel: alle Bestellungen über 10 000 Rubel auswählen und nach Datum sortieren.
Rendering und Grafikverarbeitung
- Einen Filter auf alle Pixel eines Bildes anwenden (z. B. schwarzweiß machen).
- Jeder Pixel kann unabhängig verarbeitet werden – ein idealer Fall für Parallelität.
Datenanalyse, Big Data
- MapReduce, Aggregation, Statistikberechnungen über enorme Datenmengen.
- Beispiel: Log-Verarbeitung eines Jahres zur Anomalieerkennung.
Beispiel: parallele Summenberechnung
Angenommen, wir haben ein Array mit 1 Million Zahlen. Man kann es in 4 Teile aufteilen und die Summe jedes Teils in einem separaten Thread berechnen und anschließend die Ergebnisse addieren.
4. Probleme und Herausforderungen der Parallelität
Schwierige Fehlersuche
Wenn Code in mehreren Threads läuft, können Bugs nur in seltenen Fällen auftreten, wenn Threads auf besondere Weise zusammentreffen. Manchmal tritt der Fehler einmal in 1000 Starts auf – und ihn zu erwischen ist sehr schwierig.
Datenrennen (Race Condition)
Wenn mehrere Threads gleichzeitig dieselbe Variable oder dasselbe Objekt ändern, sind inkorrekte Ergebnisse möglich. Zum Beispiel erhöhen zwei Threads gleichzeitig einen Zähler, und der Endwert ist kleiner als erwartet.
Synchronisation
Um Datenrennen zu vermeiden, müssen Sie den Zugriff auf gemeinsame Daten synchronisieren – über das Schlüsselwort synchronized, Locks, atomare Variablen und andere Werkzeuge. Das macht den Code komplexer und kann zu anderen Problemen führen (zum Beispiel Deadlock – gegenseitige Blockierung von Threads).
Lastverteilung
Wenn Sie die Aufgabe in 4 Teile geteilt haben und einer davon viel schwerer ist als die anderen – drei Threads sind bereits fertig und langweilen sich, der vierte arbeitet immer noch. Am Ende gibt es keine Beschleunigung.
Overhead
Das Starten von Threads, das Umschalten zwischen ihnen, die Synchronisation – all das braucht Zeit. Ist die Aufgabe klein, verlangsamt Parallelität die Ausführung nur.
Tabelle: Vergleich der Ansätze
| Ansatz | Wann es schnell ist | Wann es langsam ist | Beispielanwendung |
|---|---|---|---|
| Sequentiell (1 Thread) | Kleine Aufgaben, einfache Logik | Große Datenmengen | Verarbeitung von 10 Zeilen |
| Nebenläufigkeit (auf 1 Kern) | Asynchrone Aufgaben (Warten auf IO) | CPU-bound-Aufgaben (rechenlastig) auf 1 Kern | Gleichzeitiges Herunterladen von Dateien |
| Parallelität (viele Kerne) | Große, unabhängige Aufgaben | Kleine Aufgaben, starke Kopplung | Verarbeitung eines großen Arrays |
Visualisierung: wie es aussieht
// Sequentielle Verarbeitung (1 Thread)
[Aufgabe 1][Aufgabe 2][Aufgabe 3][Aufgabe 4]
// Nebenläufigkeit auf einem Kern (Umschaltlogik)
[Aufgabe 1] [Aufgabe 2] [Aufgabe 3] [Aufgabe 4]
(aber tatsächlich läuft immer nur eine nach der anderen, die übrigen warten)
// Parallelität auf vier Kernen
[Aufgabe 1] [Aufgabe 2] [Aufgabe 3] [Aufgabe 4]
(alle laufen gleichzeitig)
5. Typische Fehler beim Versuch, Parallelität einzusetzen
Fehler Nr. 1: Alles wahllos parallelisieren. Viele Einsteiger denken: „Je mehr Threads – desto schneller!“. Tatsächlich stimmt das nicht. Wenn es wenige oder sehr einfache Aufgaben gibt – gibt es keinen Gewinn, manchmal wird das Programm sogar langsamer.
Fehler Nr. 2: Synchronisation ignorieren. Wenn mehrere Threads mit denselben Daten ohne Synchronisation arbeiten – bekommen Sie Datenrennen, kaputte Logik und schwer auffindbare Fehler.
Fehler Nr. 3: Parallelität um der Parallelität willen. Parallelität ist kein Selbstzweck. Sie wird gebraucht, wenn es echte Aufgaben gibt, die sich effizient in unabhängige Teile zerlegen lassen.
Fehler Nr. 4: Besonderheiten der Aufgabe nicht berücksichtigen. Manche Aufgaben lassen sich überhaupt nicht parallelisieren (zum Beispiel wenn Schritt N+1 vom Ergebnis von Schritt N abhängt). In solchen Fällen bringt Parallelität keinen Vorteil.
Fehler Nr. 5: Overhead nicht berücksichtigen. Das Starten von Threads, das Umschalten zwischen ihnen, das Einsammeln der Ergebnisse – all das kostet Zeit. Für kleine Aufgaben kann diese Zeit größer sein als die der eigentlichen Arbeit.
GO TO FULL VERSION