Modul 12 — Concurrency & Threads
Threads: spawn, move, join
Ein Programm kann Arbeit auf mehrere Kerne verteilen: Threads. Der Start ist ein Funktionsaufruf mit einer Closure:
use std::thread;
fn main() {
let daten = vec![1, 2, 3, 4];
let handle = thread::spawn(move || {
let summe: i32 = daten.iter().sum();
summe * 10
});
println!("Ergebnis: {}", handle.join().unwrap());
}
Drei Bausteine: spawn startet die Closure auf einem neuen Thread,
move übergibt die gefangenen Daten als Besitz (nicht als Leihe —
der Thread könnte den erzeugenden Scope überleben, also würde jede
Referenz baumeln), und join() wartet auf das Ende und liefert das
Ergebnis. Vergisst du join, endet main möglicherweise zuerst — und der
Prozess nimmt den Thread mit ins Grab, bevor er fertig war.
Warum move bei Threads Pflicht ist
- Der Thread kann länger leben als die erzeugende Funktion
- Geliehene Daten könnten dangling werden — der Compiler verbietet das
- move überträgt Besitz: die Daten gehören ab jetzt dem Thread
- Das ist keine Einschränkung, sondern die Datenrennen-Prävention
📝 Schnellprüfung
Was passiert, wenn main endet, während ein Thread noch läuft?
Übung: spawn + join
🧩 Übung m12-l1-e1
🔮 Vorhersage: Was passiert beim Ausführen?
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Channels: Kommunikation statt Teilung
Der sauberste Weg, Threads Daten austauschen zu lassen: Channels — eine Einbahnstraße für Nachrichten. Go hat den Spruch geprägt: "Do not communicate by sharing memory; share memory by communicating."
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
for n in 1..=3 {
tx.send(n * n).unwrap();
}
drop(tx);
});
while let Ok(wert) = rx.recv() {
println!("{wert}");
}
}
mpsc heißt: multiple producers, single consumer — viele Sender
(Sender clonen), ein Empfänger. send überträgt Besitz der Nachricht,
recv wartet blockierend. Und das Ende ist im Typ verankert: gehen alle
Sender verloren, liefert recv ein Err — der while-let-Loop endet von
selbst. Kein Polling, keine Magic-Values: die Kanal-Schließung IST ein
Wert.
📝 Schnellprüfung
Warum endet der while-let-Loop über rx.recv() automatisch, wenn der Sender drop't?
Übung: der Quadrat-Kanal
🧩 Übung m12-l2-e1
🔮 Vorhersage: Was passiert beim Ausführen?
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Geteilter Zustand: Mutex + Arc
Manchmal willst du Daten nicht kopieren oder schicken, sondern wirklich teilen. Dann braucht es zwei Teile: einen Schloss-Mechanismus und einen thread-sicheren Besitz.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let zaehler = Arc::new(Mutex::new(0u32));
let mut handles = Vec::new();
for _ in 0..4 {
let z = Arc::clone(&zaehler);
handles.push(thread::spawn(move || {
*z.lock().unwrap() += 10;
}));
}
for h in handles {
h.join().unwrap();
}
println!("Zähler: {}", *zaehler.lock().unwrap());
}
Mutex ist das Schloss: lock() gibt dir einen Guard — nur über ihn
erreichst du die Daten, und beim Drop des Guards gibt er das Schloss
frei. Das Schloss-Umgehen ist unmöglich, die Daten liegen ja EINGESPERRT
im Mutex. Arc ist Rc mit atomarem Zähler — der einzige Grund, warum
dieselbe Struktur über Threads geteilt werden darf.
Beides zusammen ist die Standard-Antwort auf "vier Threads, ein Zähler" — und der Compiler erzwingt die Kombination: Rc-Container verweigert er mit E0277, ein Mutex ohne Teilung wäre sinnlos.
Fearless Concurrency konkret
- Datenrennen (data races) sind in safe Rust Compile-Fehler, keine crash bugs
- Send/Sync-Traits beschreiben, welche Typen thread-überqueren dürfen
- MutexGuard = scoped locking: keine vergessenen unlocks
- Deadlock-Risiko bleibt — aber Rennen verhindert der Compiler
Übung: der geteilte Zähler
🧩 Übung m12-l3-e1
🔮 Vorhersage: Was passiert beim Ausführen?
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Mini-Projekt: die parallele Summe
Der Abschluss: eine Million Zahlen, zwei Kerne, eine Antwort. Das Szenario:
🏢 Praxis-Szenario: Die Auswertung, die ewig dauert
Dein Team wertet riesige Messreihen aus — seriell, in einer Schleife, sekundenlang. Auf einem Mehrkern-Rechner arbeitet dabei 1 von 8 Kernen. Der Vorschlag: die Datenreihe halbieren und beide Hälften parallel summieren. Aber Vorsicht — naive Ansätze mit geteilten Variablen enden in Datenrennen oder Compile-Fehlern. Genau das soll die Lösung vermeiden, nicht nur vermeiden lassen.
🧩 Übung m12-l4-e1
🔮 Vorhersage: Was passiert beim Ausführen?
Was du in Modul 12 geschafft hast
- Threads mit spawn/move/join — Besitz als Ticket in den Nebenlauf
- Channels: Nachrichten mit Besitz-Übergabe, Ende im Typ verankert
- Mutex + Arc für den unvermeidlichen geteilten Zustand
- Parallelisierung durch Aufteilen statt Synchronisieren
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Modul-Checkpoint
Checkpoint: Modul 12
Bestehensgrenze: 80 %. Der Versuch wird bewertet und zählt für das Modul-Gate.
Bereit, dein Wissen zu testen?
Probiere unsere Übungsprüfungen mit Hunderten von realistischen Fragen aus.
Üben starten →