Parallelisierung wirklich verstehen — vom geteilten Adressraum über die Matrixmultiplikation bis zum Amdahlschen Gesetz, mit erklärtem C++-Code, Animationen und Quiz.
Prozess ≠ ThreadSpeedup S(N)Amdahl · fork() · waitpid()Matrixmultiplikation in C++
Bevor wir parallelisieren, klären wir das Umfeld: Ein Allzweck-Betriebssystem (GPOS) und ein Echtzeitbetriebssystem (RTOS) verfolgen gegensätzliche Ziele.
GPOS · ALLZWECK
Durchsatz & Fairness
Steuert Programme und verwaltet Ressourcen (CPU, RAM, Dateisysteme). Fokus auf hohem Datendurchsatz, Multitasking und flüssiger Bedienung — verteilt die CPU „fair".
RTOS · ECHTZEIT
Determinismus & Fristen
Spezialisiert auf Zuverlässigkeit, Vorhersehbarkeit und strikte Einhaltung zeitlicher Fristen. Strikt prioritätsbasiertes Scheduling hält Latenzen minimal.
GPOS = 🚍 Bus
Transportiert viele Leute gleichzeitig — kommt aber mal früher, mal später an.
RTOS = ⏱ Uhrwerk
Jeder Schlag erfolgt exakt zum richtigen Zeitpunkt — vorhersagbar und deterministisch.
MERKMAL
ECHTZEIT (RTOS)
ALLZWECK (GPOS)
{{ row.k }}
{{ row.rt }}
{{ row.gp }}
MERKSATZ
Ein GPOS ist wie ein Bus — es will viele „Fahrgäste" gleichzeitig transportieren, kommt aber mal früher, mal später. Ein RTOS ist wie ein Uhrwerk — jeder Schlag muss exakt zum richtigen Zeitpunkt erfolgen.
02
Durchsatz & Fairness
Durchsatz (Throughput) misst, wie viele Prozesse pro Zeiteinheit fertig werden. Er steigt, wenn der Aufwand für Kontextwechsel sinkt — doch genau hier gibt es einen Zielkonflikt mit der Fairness.
CPU-ZEITACHSE · EINE CPU, VIELE PROZESSE
{{ dSlices }} Zeitscheiben
{{ seg.tag }}
▮ Prozess-Rechenzeit▮ Kontextwechsel-Overhead
← weniger Wechsel · DurchsatzFairness · mehr Wechsel →
KONTEXTWECHSEL
{{ dSwitches }}
NUTZBARE RECHENZEIT
{{ dUsable }}%
REAKTIONSZEIT
{{ dReaction }}
MERKMAL
FOKUS FAIRNESS
FOKUS DURCHSATZ
{{ row.k }}
{{ row.fair }}
{{ row.thru }}
Ein modernes Betriebssystem sucht die Balance: flüssige Bedienung (Fairness) und schnelle Erledigung von Aufgaben (Durchsatz) zugleich.
03
Prozesse
Ein Prozess ist laut POSIX.1-2024 entweder ein aktiver Prozess oder ein Zombie-Prozess.
§ 3.189 · AKTIVER PROZESS
Ein Adressraum, in dem ein oder mehrere Threads ausgeführt werden — samt der für diese Threads erforderlichen Systemressourcen.
§ 3.426 · ZOMBIE-PROZESS
Die Überreste eines Prozesses nach seiner Beendigung — bevor der Elternprozess seine Statusinformationen verarbeitet hat.
LEBENSZYKLUS EINES KINDPROZESSES
{{ n.label }}
ELTERNPROZESS
parent
{{ zParentAction }}
waitpid() →
← Exit-Status
KINDPROZESS · PID 2041
{{ zChildState }}
{{ zBadge }}
{{ zCaption }}
Deshalb ruft der Elternprozess im matrix_processes-Programm für jedes Kind waitpid() auf: nur so werden Zombies eingesammelt und der Exit-Code geprüft.
04
Threads
Ein Thread ist ein einzelner Kontrollfluss innerhalb eines Prozesses. Mehrere Threads leben im selben Prozess — und teilen sich fast alles.
EIN PROZESS · VIER THREADS · EIN ADRESSRAUM
T1
Stack + Register
T2
Stack + Register
T3
Stack + Register
T4
Stack + Register
▼ alle greifen direkt zu ▼
GETEILTER ADRESSRAUM
Heap (malloc) · globale & statische Variablen · geöffnete Dateien · Matrizen A, B, C
PRIVAT · PRO THREAD
▸ Eigene Thread-ID
▸ Eigener Stack (automatische / lokale Variablen)
▸ Eigene Scheduling-Priorität & -Richtlinie
▸ Eigene Register / Programmzähler
GETEILT · ALLE THREADS
▸Heap — über malloc() bereitgestellter Speicher
▸ Globale und statische Variablen
▸ Geöffnete Dateien & Ressourcen
▸ Jede Adresse, die ein Thread ermitteln kann
ACHTUNG
Weil der Speicher geteilt ist, ist Kommunikation extrem schnell (direkter Zugriff) — erfordert aber Synchronisation (Mutex, Semaphore), um Race Conditions zu vermeiden.
05
Threads vs. Prozesse
Der entscheidende Unterschied ist der Adressraum. Threads teilen ihn — Prozesse nicht. Spiel die Animation ab und achte auf die Festplatten-Lesevorgänge.
C = A × B · 512×512 · 4 Einheiten
THREADS1 Prozess
🖴 Festplatte · A · B
{{ tReadLabel }}
GETEILTER SPEICHER
A · B · C — nur eine Kopie
▲ ▲ ▲ ▲
T1
T2
T3
T4
Festplatten-Lesevorgänge: {{ tReads }}
PROZESSEN Kindprozesse
🖴 Festplatte · A · B
{{ pReadLabel }}
{{ pp.name }}
A·B eigene Kopie
{{ pTempLabel }}
{{ pAssembleLabel }}
Festplatten-Lesevorgänge: {{ pReads }}
{{ vsCaption }}
MERKMAL
THREADS
PROZESSE
{{ row.k }}
{{ row.t }}
{{ row.p }}
06
POSIX & Programm
POSIX.1-2024 (IEEE Std 1003.1) definiert einheitliche Betriebssystemschnittstellen. Sein Hauptziel: Quellcode-Portabilität.
EINMAL SCHREIBEN — ÜBERALL AUSFÜHREN
main.cpp
POSIX-API: fork() · waitpid()
kompiliert & läuft unverändert
⟶
Linux
macOS
BSD / Unix
§ 3.291 · PROGRAMM
Eine vorbereitete Abfolge von Anweisungen an das System zur Ausführung einer definierten Aufgabe.
Umfasst u.a. Shell-Skripte, Eingabesprachen (awk, lex, sed) und Hochsprachen wie C++.
POSIX-SYSTEMAUFRUFE IM PROJEKT
fork()erzeugt einen Kindprozess (man 2 fork)
waitpid()wartet auf ein Kind (man 2 waitpid)
getpid()liefert die eigene Prozess-ID
std::exit()beendet den Kindprozess sofort
07
Matrixmultiplikation
Das Rechenproblem des Projekts: C = A × B. Jedes Element von C ist ein Skalarprodukt aus einer Zeile von A und einer Spalte von B.
Jede Zeile von C hängt nur von A und B ab — nicht von anderen Zeilen. Deshalb lässt sich die Berechnung problemlos zeilenweise auf mehrere Threads oder Prozesse aufteilen.
08
Das pt-edu Projekt
Dasselbe Problem — C = A × B — einmal mit Threads, einmal mit Prozessen. Beide teilen die Zeilen von C auf ihre Arbeiter auf.
A und B werden einmalig geladen. Alle Threads teilen die Daten im Speicher, berechnen je einen Zeilenbereich und schreiben in den gemeinsamen Speicher.
PROZESS-VARIANTE
Der Elternprozess forkt N Kinder. Jedes liest A und B selbst, berechnet seinen Zeilenbereich und schreibt eine Temp-Datei. Der Elternprozess setzt C zusammen.
Im Code: rowEnd = (i == numThreads-1) ? A.rows : (i+1)*rowsPerThread — der letzte Arbeiter bekommt immer den Rest.
09
Speedup
Der Speedup misst, wie viel schneller ein Programm mit N parallelen Einheiten läuft — das Verhältnis von serieller zu paralleler Ausführungszeit.
S(N)=T(1)T(N)serielle Zeit ÷ parallele Zeit
GEMESSEN · THREADS · 256×256
N=1 · 8,29 ms
1,00×
N=2 · 4,94 ms
1,68×
N=8 · 3,30 ms
2,51×
Der reale Speedup bleibt deutlich unter der idealen Linie S=N. Von 1→2 Threads verdoppelt sich fast die Leistung, von 2→8 kaum noch. Warum? Das erklärt das Amdahlsche Gesetz.
10
Amdahlsches Gesetz
Warum bringt der achte Thread kaum noch etwas? Weil ein Teil jedes Programms seriell bleibt. Amdahl beziffert die Obergrenze: Der parallelisierbare Anteil p entscheidet, wie viel Beschleunigung überhaupt möglich ist — egal wie viele Kerne.
S(N)=1(1−p) + p/N
SPEEDUP-KURVE · SCHIEBE DEN PARALLELEN ANTEIL
p = {{ amP }} %
0 % · alles seriell99 % · fast alles parallel
PARALLELER ANTEIL
{{ amP }} %
MAX · N → ∞
{{ amMax }}
BEI N = 8
{{ amS8 }}
{{ amCaption }}
MERKSATZ
Nicht die Zahl der Kerne begrenzt den Speedup, sondern der serielle Rest. Bei 90 % parallel ist bei 10× Schluss — selbst mit 1000 Kernen. Deshalb lohnt es sich, den seriellen Anteil zu verkleinern, statt nur Kerne hinzuzufügen.
11
Der Code, erklärt
Vier Kernstücke aus pt-edu. Klapp jedes auf — der Code oben, die Idee in Worten darunter. Thread- und Prozess-Variante teilen sich dieselbe Worker-Logik.
// matrix_multiply.cpp — rechnet die Zeilen [rowStart, rowEnd)void multiplyRange(const Matrix& A, const Matrix& B,
Matrix& C, int rowStart, int rowEnd) {
for (int i = rowStart; i < rowEnd; ++i)
for (int j = 0; j < B.cols; ++j) {
long sum = 0;
for (int l = 0; l < A.cols; ++l)
sum += A.at(i, l) * B.at(l, j);
C.at(i, j) = sum; // schreibt in gemeinsames C
}
}
Jeder Arbeiter bekommt nur seinen Bereich [rowStart, rowEnd). Die innerste Schleife bildet das Skalarprodukt aus Zeile i von A und Spalte j von B — genau die Formel aus Kapitel 07. Weil jede Zeile von C unabhängig ist, kommen sich die Arbeiter nie in die Quere.
// main.cpp (Thread-Variante) — starten & einsammelnint rowsPerThread = A.rows / numThreads;
std::vector<std::thread> pool;
for (int t = 0; t < numThreads; ++t) {
int start = t * rowsPerThread;
int end = (t == numThreads - 1) ? A.rows
: start + rowsPerThread;
pool.emplace_back(multiplyRange, std::cref(A), std::cref(B),
std::ref(C), start, end);
}
for (auto& th : pool) th.join(); // auf alle warten
Alle Threads teilen sich A, B und C im selben Adressraum — deshalb werden sie per std::cref / std::ref als Referenz übergeben, ohne zu kopieren. Der letzte Thread bekommt A.rows als Ende, damit keine Zeile fehlt. join() blockiert, bis jeder Thread fertig ist.
// main.cpp (Prozess-Variante) — Kinder abspaltenfor (int p = 0; p < numProcs; ++p) {
pid_t pid = fork(); // Prozess duplizierenif (pid == 0) { // 0 = wir sind das Kindint start = p * rowsPerProc;
int end = (p == numProcs - 1) ? A.rows
: start + rowsPerProc;
computeAndWriteTemp(A, B, start, end, p);
std::exit(0); // Kind beendet sich sauber
}
children.push_back(pid); // Eltern merkt sich die PID
}
fork() dupliziert den Prozess: im Kind liefert es 0, im Elternprozess die PID des Kindes. Jedes Kind hat seinen eigenen Adressraum, liest A und B selbst und schreibt sein Teilergebnis in eine Temp-Datei. std::exit(0) beendet das Kind — sonst liefe es in der Schleife des Elternteils weiter und würde selbst forken.
// Eltern: auf alle Kinder warten, Zombies einsammelnfor (pid_t pid : children) {
int status;
waitpid(pid, &status, 0); // blockiert bis Kind endetif (WIFEXITED(status) && WEXITSTATUS(status) != 0)
std::cerr << "Kind " << pid << " fehlgeschlagen";
}
assembleResult(C, numProcs); // Temp-Dateien -> C
waitpid() holt den Exit-Status jedes Kindes ab — genau der „reaping“-Vorgang aus Kapitel 03. WIFEXITED und WEXITSTATUS prüfen, ob das Kind sauber mit Code 0 endete. Erst danach liest der Elternprozess die Temp-Dateien und setzt C zusammen.
12
Die Aufgaben
Fünf Fragen zum Projekt — erst selbst überlegen, dann die Musterlösung aufklappen.
{{ t.num }}
{{ t.title }}{{ t.tag }}
{{ t.prompt }}
MUSTERLÖSUNG
{{ t.solution }}
13
Fachwortglossar
Die wichtigsten Begriffe EN → DE. Tippe zum Filtern.
⌕
{{ glCount }} / {{ glTotal }} Begriffe
{{ g.en }}→ {{ g.de }}
{{ g.def }}
Kein Begriff passt zu „{{ glQuery }}“.
14
Quiz
Fünf Fragen quer durch alle Kapitel. Tippe eine Antwort an — richtig oder falsch erscheint sofort, samt Begründung.