Kapitel 16: Nebenläufigkeit und asynchrone Programmierung
Moderne CPUs besitzen heute fast immer mehrere Prozessorkerne. Um die maximale Leistung aus einer Maschine herauszuholen, müssen Programme Aufgaben parallel oder nebenläufig ausführen. Gleichzeitig stellt uns die Nebenläufigkeit (Concurrency) vor massive Herausforderungen: Datenrennen (Data Races), Deadlocks (gegenseitige Blockaden) und unvorhersehbares Verhalten zur Laufzeit sind in traditionellen Sprachen wie C oder C++ an der Tagesordnung.
Rust wurde mit dem Versprechen angetreten, diese Fehlerklasse komplett auszurotten. Das Konzept nennt sich Fearless Concurrency (angstfreie Nebenläufigkeit). Der Compiler sorgt mithilfe seines Typsystems (Ownership, Borrowing und spezielle Marker-Traits) dafür, dass fehlerhafter, threadsicherheits-verletzender Code erst gar nicht kompiliert wird.
Zusätzlich bietet Rust ein modernes asynchrones Programmiermodell (async/await), mit dem Tausende von Aufgaben ressourcenschonend auf einer Handvoll Threads ausgeführt werden können.
In diesem Kapitel bieten wir Ihnen drei verschiedene Perspektiven auf das Thema an. Wählen Sie die Sicht, die am besten zu Ihrem Hintergrund passt:
- Für Anfänger (Einfach): Konzentriert sich auf das Orchester-Prinzip, Threads erstellen mit
thread::spawn, Kanäle (Channels) für die Kommunikation und Mutexes in Kombination mitArc. - für Profis (Architektur): Behandelt
SendundSync, fortgeschrittene Synchronisation (RwLock,Condvar), lockfreie Programmierung mit Atomics, Interior Mutability (RefCell) und asynchrone Programmierung mit der Tokio-Runtime. - Hardware-Sicht (CPU/RAM): Analysiert, was bei
thread::spawnim RAM passiert, Cache-Kohärenz (MESI-Protokoll), CPU-Befehle wie Compare-And-Swap (CAS), Mutex-Vergiftung und die Funktionsweise asynchroner Futures als Zustandsautomaten.
Begleitvideo zu Kapitel 16: Nebenläufigkeit und asynchrone Programmierung
Kapitel 16: Nebenläufigkeit und asynchrone Programmierung – Das große Orchester
Stell dir vor, du bist der Dirigent eines großen Orchesters. Auf der Bühne sitzen Dutzende Musiker: Geiger, Cellisten, Flötisten und Trommler.
Wenn jeder Musiker einfach drauflosspielen würde, sobald er Lust hat, gäbe es ein furchtbares Durcheinander. Der Trommler würde den Geiger übertönen, und niemand wüsste, wann sein Einsatz ist. Das Ergebnis wäre ohrenbetäubender Lärm.
Als Dirigent sorgst du für Ordnung:
- Arbeitsteilung: Jeder Musiker (in unserem Code ein Thread) hat seine eigenen Noten (seine Aufgabe).
- Synchronisation: Du gibst den Takt vor, damit alle Musiker zur gleichen Zeit spielen.
- Kommunikation: Über Blicke und Handzeichen gibst du Einsätze weiter, ohne dass die Musiker aufstehen und miteinander reden müssen.
In der Programmierung ist das ähnlich. Dein Computer hat mehrere Prozessorkerne (Musiker). Um dein Programm schneller zu machen, möchtest du Aufgaben gleichzeitig ausführen lassen. Wenn aber zwei Threads gleichzeitig versuchen, dieselbe Variable im Speicher zu ändern, gibt es ein Chaos – eine sogenannte Race Condition (Datenrennen).
Rusts Typsystem wirkt hier wie ein unbestechlicher Dirigent: Es sorgt zur Compilezeit dafür, dass sich deine Threads niemals in die Quere kommen!
1. Lernziele – Das wirst du heute lernen
- Threads erstellen: Du lernst, wie du mit
thread::spawnneue Musiker auf die Bühne holst. - Das move-Schlüsselwort verstehen: Du erfährst, warum Threads den Besitz von Variablen übernehmen müssen.
- Kanäle nutzen (Channels): Du lässt Threads sicher Nachrichten austauschen.
- Daten absichern mit Mutex: Du verhinderst, dass zwei Threads gleichzeitig dieselbe Variable verändern.
- Arc verstehen: Du lernst den atomaren Referenzzähler kennen, der Daten an mehrere Threads verteilt.
2. Threads erstellen: thread::spawn
In Rust entspricht ein Thread einem echten Betriebssystem-Thread. Wir erstellen einen Thread mit der Funktion thread::spawn und übergeben ihr eine Closure (eine anonyme Funktion) mit dem auszuführenden Code.
use std::thread;
use std::time::Duration;
fn main() {
// Wir starten einen neuen Thread!
let handle = thread::spawn(|| {
for i in 1..5 {
println!("Hallo aus dem spawn-Thread: {}", i);
// Wir legen den Thread kurz schlafen, damit der Hauptthread auch dran kommt
thread::sleep(Duration::from_millis(50));
}
});
// Code im Hauptthread läuft gleichzeitig!
for i in 1..3 {
println!("Hallo aus dem Hauptthread: {}", i);
thread::sleep(Duration::from_millis(30));
}
// WICHTIG: Wir warten, bis der spawn-Thread komplett fertig ist.
// Ohne join() würde das Programm beendet, sobald main() fertig ist,
// selbst wenn der spawn-Thread noch mitten in der Arbeit steckt!
handle.join().unwrap();
println!("Programm beendet.");
}
Das move-Schlüsselwort bei Threads
Wenn ein spawn-Thread eine Variable aus seiner Umgebung nutzen möchte, müssen wir dem Thread die Variable schenken. Da der Thread theoretisch länger leben könnte als die main-Funktion, erlaubt Rust keine Referenzen auf lokale Variablen im Thread.
Wir nutzen das Schlüsselwort move, um den Besitz der Variable komplett in den Thread zu übertragen:
use std::thread;
fn main() {
let botschaft = String::from("Geheime Nachricht");
// Mit 'move' zieht die botschaft komplett in den Thread um
let handle = thread::spawn(move || {
println!("Botschaft im Thread empfangen: {}", botschaft);
});
// FEHLER! botschaft gehört uns hier nicht mehr!
// println!("{}", botschaft);
handle.join().unwrap();
}
3. Channels: Sichere Kommunikation über Postboten
Ein wichtiges Prinzip in Rust lautet: „Kommuniziere nicht, indem du Speicher teilst. Teile stattdessen Speicher, indem du kommunizierst.“
Statt dass mehrere Threads auf derselben Variable herumreiten, schicken sie sich lieber Briefe über einen Datenkanal (Channel). Ein Channel hat zwei Enden:
- Den Sender (
txfür transmit) - Den Empfänger (
rxfür receive)
Rust bietet hierfür das Modul std::sync::mpsc (Multiple Producer, Single Consumer). Das bedeutet: Es kann viele Sender geben, aber nur einen Empfänger!
use std::sync::mpsc;
use std::thread;
fn main() {
// Wir erstellen den Kanal. tx = Sender, rx = Empfänger
let (tx, rx) = mpsc::channel();
// Wir starten einen Thread, der Daten sendet
thread::spawn(move || {
let nachricht = String::from("Kaffee ist fertig!");
// send() schickt die Nachricht ab und übergibt das Eigentum daran!
tx.send(nachricht).unwrap();
});
// Im Hauptthread warten wir auf die Nachricht
// recv() blockiert den Hauptthread, bis etwas ankommt
let erhalten = rx.recv().unwrap();
println!("Hauptthread meldet: {}", erhalten);
}
4. Geteilter Speicher mit Mutex und Arc
Manchmal lässt es sich nicht vermeiden: Mehrere Threads müssen tatsächlich an derselben Variable arbeiten (z. B. ein globaler Zähler, der von 10 Threads erhöht werden soll).
Wenn zwei Threads gleichzeitig versuchen, eine Zahl zu erhöhen (auslesen, 1 addieren, zurückschreiben), kann es passieren, dass einer die Änderung des anderen überschreibt.
Rust löst das mit einem Mutex (Mutual Exclusion / gegenseitiger Ausschluss). Ein Mutex ist wie ein Bankschließfach: Nur wer den Schlüssel (das Lock) hat, darf an die Daten.
Damit mehrere Threads auf das Schließfach zugreifen können, verpacken wir den Mutex zusätzlich in einen atomaren Referenzzähler (Arc). Arc erlaubt es, das Eigentum am Mutex sicher aufzuteilen.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// Arc = Atomarer Referenzzähler (erlaubt geteilten Besitz)
// Mutex = Das Schließfach um unsere Daten (die 0)
let zaehler = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
// Wir erstellen einen Klon von Arc.
// Das erhöht nur den Zähler, die Daten im Mutex werden NICHT kopiert!
let zaehler_klon = Arc::clone(&zaehler);
let handle = thread::spawn(move || {
// lock() wartet, bis das Schließfach frei ist, und sperrt es.
// Es gibt einen "Guard" zurück, der uns Zugriff gewährt.
let mut daten = zaehler_klon.lock().unwrap();
*daten += 1;
// Sobald die Variable 'daten' am Ende des Blocks out of Scope geht,
// wird das Schließfach automatisch wieder zugesperrt! (RAII-Prinzip)
});
handles.push(handle);
}
// Auf alle Threads warten
for handle in handles {
handle.join().unwrap();
}
// Wert auslesen
println!("Endergebnis: {}", *zaehler.lock().unwrap()); // 10
}
5. Compilerfehler-Show: Threadsicherheit erzwingen
Was passiert, wenn wir versuchen, den normalen Referenzzähler Rc (den wir in Single-Thread-Programmen nutzen) an einen Thread zu übergeben?
use std::rc::Rc;
use std::thread;
fn main() {
let daten = Rc::new(5);
let daten_klon = Rc::clone(&daten);
thread::spawn(move || {
println!("{}", daten_klon);
});
}
Die Fehlermeldung des Compilers:
error[E0277]: `Rc<i32>` cannot be sent between threads safely
--> src/main.rs:8:19
|
8 | thread::spawn(move || {
| _____-------------_^
| | |
| | required by a bound introduced by this call
9 | | println!("{}", daten_klon);
10 | | });
| |_____^ `Rc<i32>` cannot be sent between threads safely
|
= help: the trait `Send` is not implemented for `Rc<i32>`
Die Erklärung:
Der Compiler rettet uns hier das Leben! Er sieht, dass Rc nicht das Marker-Trait Send implementiert. Warum? Weil Rc den Referenzzähler mit normalen mathematischen Operationen erhöht. Würden zwei Threads gleichzeitig Rc::clone aufrufen, könnte der Zähler beschädigt werden, was zu Speicherlecks oder Abstürzen führt.
Die Lösung: Ersetze Rc durch Arc. Arc nutzt spezielle atomare CPU-Befehle, die absolut threadsicher sind.
6. Zusammenfassung
- Threads führen Aufgaben parallel auf verschiedenen CPU-Kernen aus. Mit
thread::spawnstarten wir sie. - Das Schlüsselwort
moveübergibt den Besitz von Variablen an den spawn-Thread. - Channels (
mpsc) erlauben sichere Kommunikation zwischen Threads über Nachrichten. - Ein Mutex sichert geteilte Daten ab. Nur ein Thread darf das Lock halten.
Arc(Atomic Reference Counted) ist die threadsichere Variante vonRcund ermöglicht geteilten Besitz an Ressourcen über Thread-Grenzen hinweg.
Kapitel 16: Nebenläufigkeit und asynchrone Programmierung – Threadsicherheit und Asynchronität
Der Entwurf hochperformanter, nebenläufiger Systeme erfordert ein tiefes Verständnis der Typsicherheitsgarantien des Compilers sowie die Fähigkeit, zwischen Thread-basierter Nebenläufigkeit und asynchronem I/O abzuwägen.
Während Threads ideal für CPU-intensive Berechnungen (z. B. Bildverarbeitung, mathematische Simulationen) sind, eignet sich die asynchrone Programmierung perfekt für I/O-intensive Aufgaben (z. B. Webserver, Datenbankabfragen), bei denen das Programm die meiste Zeit auf externe Antworten wartet.
1. Lernziele – Das wirst du heute lernen
- Send und Sync beherrschen: Sie verstehen die Funktionsweise von Marker-Traits und Auto-Traits.
- Fortgeschrittene Synchronisation: Sie setzen
RwLockundCondvargezielt ein. - Lockfreie Programmierung: Sie nutzen atomare Operationen zur Synchronisation ohne Sperren.
- Interior Mutability mit RefCell: Sie nutzen innere Veränderlichkeit zur Laufzeit.
- Asynchrone Syntax (
async/await): Sie schreiben I/O-effizienten asynchronen Code. - Die Funktionsweise von Runtimes: Sie steuern asynchrone Aufgaben über die Tokio-Runtime.
2. Die Marker-Traits Send und Sync
Das Fundament der Threadsicherheitsgarantie in Rust bilden zwei eingebaute Marker-Traits (Traits, die keine Methoden deklarieren, sondern nur semantische Eigenschaften markieren):
Send: Ein Typ istSend, wenn der Besitz an diesem Typ sicher an einen anderen Thread übertragen werden darf.Sync: Ein Typ istSync, wenn es sicher ist, Referenzen auf diesen Typ (&T) von mehreren Threads zeitgleich lesend zu verwenden.
Wichtige Zusammenhänge:
- Ein Typ
Tist genau dannSync, wenn seine unveränderliche Referenz&TSendist. - Fast alle primitiven Typen sind
SendundSync. Rc<T>ist wederSendnochSync, da die Referenzzähler-Updates nicht atomar sind.RefCell<T>istSend, aber nichtSync, da die Ausleihprüfung zur Laufzeit nicht threadsicher ist.- Marker-Traits sind Auto-Traits: Der Compiler implementiert
SendundSyncautomatisch für Ihre Datenstrukturen, sofern alle darin enthaltenen Typen ebenfalls diese Eigenschaften besitzen.
3. Fortgeschrittene Synchronisationsprimitive
RwLock (Reader-Writer Lock)
Ein RwLock verhält sich analog zu Rusts Aliasing-Regeln: Es erlaubt entweder beliebig viele parallele Leser ODER maximal einen Schreiber exklusiv. Dies ist deutlich effizienter als ein standardmäßiger Mutex, wenn Daten häufig gelesen, aber selten verändert werden.
use std::sync::RwLock;
fn main() {
let daten = RwLock::new(5);
// Paralleler Lesezugriff
{
let r1 = daten.read().unwrap();
let r2 = daten.read().unwrap();
println!("Lesewerte: {}, {}", *r1, *r2);
} // Lese-Sperren erlöschen hier
// Exklusiver Schreibzugriff
{
let mut w = daten.write().unwrap();
*w += 1;
}
}
Condvar (Bedingungsvariablen)
Eine Condvar erlaubt es einem Thread, so lange ressourcenschonend zu blockieren (zu schlafen), bis eine bestimmte Bedingung eintritt. Sie wird immer zusammen mit einem Mutex betrieben:
use std::sync::{Arc, Mutex, Condvar};
use std::thread;
fn main() {
let start_signal = Arc::new((Mutex::new(false), Condvar::new()));
let signal_klon = Arc::clone(&start_signal);
thread::spawn(move || {
let (lock, cvar) = &*signal_klon;
let mut gestartet = lock.lock().unwrap();
*gestartet = true;
cvar.notify_one(); // Den wartenden Thread aufwecken
});
let (lock, cvar) = &*start_signal;
let mut gestartet = lock.lock().unwrap();
// Schleife schützt vor Spurious Wakeups (Fehlalarmen des OS)
while !*gestartet {
gestartet = cvar.wait(gestartet).unwrap(); // Gibt das Lock temporär frei
}
println!("Start-Signal erhalten!");
}
4. Lockfreie Programmierung mit Atomics
Für einfache numerische Operationen ist das Sperren eines Mutex oft zu rechenintensiv, da es Betriebssystem-Systemaufrufe involviert. Rust bietet im Modul std::sync::atomic Typen an, die direkt über CPU-Befehle synchronisiert werden:
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
fn main() {
let zaehler = Arc::new(AtomicUsize::new(0));
let mut handles = vec![];
for _ in 0..10 {
let klon = Arc::clone(&zaehler);
let handle = thread::spawn(move || {
// fetch_add arbeitet threadsicher auf Hardware-Ebene
klon.fetch_add(1, Ordering::Relaxed);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Stand: {}", zaehler.load(Ordering::Relaxed)); // 10
}
Hinweis zur Speicherordnung (Ordering): Wir nutzen hier Ordering::Relaxed, was dem Compiler erlaubt, die Speicheroperationen im Sinne der Performance umzusortieren. Für komplexe lockfreie Datenstrukturen müssen stärkere Garantien wie Acquire und Release verwendet werden.
5. Asynchrones Rust: async und await
Im Gegensatz zu OS-Threads sind asynchrone Tasks extrem leichtgewichtig. Sie verhalten sich wie kooperative Green Threads. Das Umschalten zwischen Tasks erfordert keinen Context Switch des Betriebssystems.
Die async/await-Syntax
Eine async fn gibt eine Struktur zurück, die das Future-Trait implementiert. Ein Future ist ein Zustandsautomat, der auf seine Auswertung wartet.
#![allow(unused)]
fn main() {
// Eine asynchrone Funktion
async fn daten_laden() -> String {
// Simuliert asynchrones Warten
String::from("Daten fertig geladen")
}
async fn verarbeiten() {
// .await pausiert den aktuellen Task (gibt die CPU frei),
// bis das Future aufgelöst ist.
let ergebnis = daten_laden().await;
println!("{}", ergebnis);
}
}
Die Runtime (Tokio)
Da Rusts Standardbibliothek keinen eingebauten Executor besitzt, müssen wir eine externe Runtime wie Tokio nutzen:
#[tokio::main]
async fn main() {
// Nebenläufige Ausführung von zwei asynchronen Tasks
let task1 = daten_laden();
let task2 = daten_laden();
let (res1, res2) = tokio::join!(task1, task2);
println!("Ergebnisse: {} & {}", res1, res2);
}
Kapitel 16 - Hardware-Sicht: Concurrency unter der Lupe von CPU, RAM und Kernel
Hallo Thorsten! Nachdem wir uns mit den Abstraktionen der Threadsicherheit und asynchronen Runtimes beschäftigt haben, reißen wir jetzt die Motorhaube auf und analysieren, wie Nebenläufigkeit physikalisch auf CPU- und Speicher-Ebene abgebildet wird.
Als Systemprogrammierer gibst du dich nicht mit der Erklärung „Es läuft gleichzeitig“ zufrieden. Du willst wissen: Wie sieht der Context Switch im CPU-Kern aus? Wie verhalten sich L1- und L2-Caches bei geteiltem Speicher? Und wie werden asynchrone Futures hardwareseitig abgebildet?
Schnapp dir einen Kaffee – wir steigen tief ab!
1. Was passiert bei thread::spawn auf Betriebssystem-Ebene?
Wenn du thread::spawn aufrufst, delegiert Rust diese Aufgabe direkt an das Betriebssystem (unter Linux wird der Systemaufruf clone mit entsprechenden Flags verwendet).
Die physikalischen Kosten eines OS-Threads:
- Stack-Allokation: Das Betriebssystem reserviert für den neuen Thread einen eigenen Speicherbereich auf dem Stack (standardmäßig meist 2 Megabyte unter Linux). Dieser Speicher muss im virtuellen Adressraum verwaltet werden.
- Context Switch (Kontextwechsel): Wenn die CPU zwischen Threads hin- und herspaltet, muss der aktuelle Zustand des CPU-Kerns gesichert werden:
- Die CPU-Register (Program Counter, Stack Pointer, allgemeine Register) werden in den Speicher geschrieben.
- Die Page Tables (Speicherübersetzungstabellen) des MMU (Memory Management Unit) müssen eventuell getauscht werden.
- Der Instruction- und Data-Cache des CPU-Kerns ist für den neuen Thread ungültig, was anfangs zu massiven Cache-Misses führt.
- Scheduler-Overhead: Der Betriebssystem-Scheduler muss ständig Berechnungen anstellen, welcher Thread als Nächstes CPU-Zeit erhält.
Systemprogrammierer-Regel: Erstelle OS-Threads niemals in einer engen Schleife für kleine Aufgaben. Nutze stattdessen Thread-Pools (z. B. das Crate rayon) oder asynchrone Programmierung.
2. Cache-Kohärenz und das MESI-Protokoll
Moderne Mehrkern-CPUs besitzen pro Kern extrem schnelle L1- und L2-Caches. Wenn zwei verschiedene CPU-Kerne dieselbe Variable aus dem RAM lesen, kopieren sie diese in ihre jeweiligen Caches.
Das Problem: Cache Line Bouncing (False Sharing)
Wenn Kern 1 die Variable ändert, muss er diese Änderung an Kern 2 signalisieren, da Kern 2 sonst veraltete Daten lesen würde.
Die CPUs nutzen hierfür das MESI-Protokoll (Modified, Exclusive, Shared, Invalid):
- Wenn Kern 1 den Wert ändert, markiert er die entsprechende Cache Line (meist 64 Bytes groß) in seinem Cache als Modified.
- Gleichzeitig schickt er ein Signal über den internen Prozessorbus, um diese Cache Line im Cache von Kern 2 als Invalid zu markieren.
- Möchte Kern 2 nun wieder auf die Variable zugreifen, bemerkt er den Invalid-Zustand. Die CPU muss den Befehl anhalten und die Daten mühsam aus dem L3-Cache oder dem Hauptspeicher nachladen (Cache Line Bouncing).
Wenn zwei Threads auf unterschiedlichen Kernen ständig dieselbe Speicheradresse oder benachbarte Speicheradressen in derselben Cache Line ändern, bricht die Performance dramatisch ein.
3. Wie Atomics auf CPU-Ebene funktionieren
Atomare Typen (AtomicUsize etc.) verzichten auf Mutexes und Betriebssystem-Sperren. Sie kommunizieren direkt mit der Hardware.
1. Compare-And-Swap (CAS)
Auf x86-Prozessoren kompiliert eine atomare Operation oft zu dem Befehl LOCK CMPXCHG. Das Präfix LOCK signalisiert dem Speicherbus der CPU: „Reserviere den Zugriff auf diese Speicheradresse exklusiv für meinen Kern für die Dauer dieses Befehls.“ Kein anderer Kern darf während dieses CPU-Takts auf diese Adresse zugreifen.
2. Speicherbarrieren (Memory Barriers / Fences)
Moderne CPUs führen Befehle zur Performance-Steigerung nicht immer in der geschriebenen Reihenfolge aus (Out-of-Order Execution).
Speicherordnungen (wie Ordering::SeqCst oder Ordering::Acquire/Release) zwingen die CPU, sogenannte Memory Barriers (Speicherbarrieren) in den Befehlsstrom einzufügen. Diese Barrieren verhindern, dass Lese- oder Schreibbefehle vor oder hinter die Barriere rutschen. Das sorgt für korrekte Programmabläufe, bremst aber die Out-of-Order-Optimierung der CPU aus.
4. Die Hardware-Sicht auf Async Rust (Futures als State Machines)
Im Gegensatz zu Green Threads in Sprachen wie Go oder Java, die ebenfalls einen eigenen Stack pro Task allokieren, arbeitet Async Rust stackless (stacklos).
Wie funktioniert das?
Wenn Sie eine asynchrone Funktion schreiben, übersetzt der Rust-Compiler diese in einen statischen Zustandsautomaten (eine automatisch generierte Struktur).
#![allow(unused)]
fn main() {
// Quellcode:
async fn beispiel() {
schritt_1().await;
schritt_2().await;
}
}
Der Compiler macht daraus eine Struktur, die ungefähr so aussieht:
#![allow(unused)]
fn main() {
enum BeispielFutureState {
Start,
WarteAufSchritt1(Schritt1Future),
WarteAufSchritt2(Schritt2Future),
Fertig,
}
}
Die Hardware-Vorteile:
- Kein Stack-Overhead: Ein Future benötigt keinen reservierten Stack-Speicher. Es ist so groß wie der größte Zustand im Zustandsautomaten.
- Keine Heap-Allokationen: Futures können auf dem Stack der übergeordneten Funktion leben oder in einem einzigen großen Block (z. B. im Task-Queue der Runtime) allokiert werden.
- Hocheffizienter Context Switch: Wenn ein Future
Poll::Pendingzurückgibt, speichert die Runtime nur den winzigen Enum-Zustand. Der Wechsel zum nächsten Task ist ein einfacher Funktionsaufruf (kein Tausch von CPU-Registern oder Page Tables nötig).
4. Verweis auf Übungen
Sie haben nun gelernt, wie Sie Threads erzeugen, Daten absichern und wie diese Vorgänge physikalisch auf CPU- und Speicherebene ablaufen. Jetzt ist es an der Zeit, dieses Wissen praktisch zu vertiefen.
Wechseln Sie in das Verzeichnis: exercises/04_collections/ (oder ein entsprechendes Concurrency-Verzeichnis Ihres Übungs-Workspaces).
Dort finden Sie praktische Aufgaben, bei denen Sie:
- Berechnungen auf mehrere Threads aufteilen und Ergebnisse einsammeln müssen.
- Einen sicheren Daten-Kanal (Channel) zur Thread-Kommunikation einrichten.
- Einen geteilten Zähler mittels
Arc<Mutex<T>>threadsicher implementieren.