Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. Arbeitsteilung: Jeder Musiker (in unserem Code ein Thread) hat seine eigenen Noten (seine Aufgabe).
  2. Synchronisation: Du gibst den Takt vor, damit alle Musiker zur gleichen Zeit spielen.
  3. 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::spawn neue 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 (tx für transmit)
  • Den Empfänger (rx fü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

  1. Threads führen Aufgaben parallel auf verschiedenen CPU-Kernen aus. Mit thread::spawn starten wir sie.
  2. Das Schlüsselwort move übergibt den Besitz von Variablen an den spawn-Thread.
  3. Channels (mpsc) erlauben sichere Kommunikation zwischen Threads über Nachrichten.
  4. Ein Mutex sichert geteilte Daten ab. Nur ein Thread darf das Lock halten.
  5. Arc (Atomic Reference Counted) ist die threadsichere Variante von Rc und ermöglicht geteilten Besitz an Ressourcen über Thread-Grenzen hinweg.