Parallelität und Multithreading in Rust: Fortgeschrittene Techniken

Online Python-Trainer für Anfänger

Lernen Sie Python einfach ohne theoretische Überlastung. Lösen Sie praktische Aufgaben mit automatischer Überprüfung und schreiben Sie Code direkt im Browser.

Kurs starten

Parallelität und Multithreading in Rust: Fortgeschrittenes Niveau

\n\n

Parallelität und Multithreading gehören zu den mächtigsten und gleichzeitig komplexesten Aspekten der Systemprogrammierung. Rust bietet einen einzigartigen Ansatz: Speichersicherheitsgarantien zur Kompilierzeit, die Datenrennen (data race) und hängende Zeiger ausschließen. In diesem Artikel behandeln wir fortgeschrittene Techniken für die Arbeit mit Parallelität in Rust, einschließlich async/await, der Bibliothek Tokio, den Traits Send und Sync sowie Low-Level-Synchronisationsprimitive.

\n\n

Das Material richtet sich an Entwickler, die bereits mit den Grundlagen von Rust vertraut sind und sich in hochperformante parallele Berechnungen vertiefen möchten.

\n\n

1. Parallelitätsmodelle: OS-Threads und Asynchronität

\n\n

Rust unterstützt zwei Hauptmodelle: Betriebssystem-Threads (OS threads) und asynchrone Aufgaben (async tasks). Die Wahl zwischen ihnen hängt vom Szenario ab: Für CPU-intensive Aufgaben sind Threads besser geeignet, für I/O-gebundene Aufgaben eignet sich async.

\n\n

OS-Threads (std::thread)

\n\n

Das Erstellen eines Threads in Rust ist trivial, erfordert jedoch Aufmerksamkeit hinsichtlich des Datenbesitzes:

\n\n
use std::thread;\n\nlet data = vec![1, 2, 3];\n\nlet handle = thread::spawn(move || {\n    println!("{:?}", data);\n});\n\nhandle.join().unwrap();
\n\n

Der entscheidende Punkt ist das Schlüsselwort move. Es übergibt den Besitz an die Closure. Wenn Daten zwischen Threads geteilt werden sollen, verwenden Sie Arc<Mutex<T>>.

\n\n

Asynchrone Programmierung (async/await)

\n\n

Asynchroner Code arbeitet innerhalb eines einzelnen Threads und wechselt zwischen Aufgaben während I/O-Wartezeiten. Dies reduziert den Overhead durch Kontextwechsel. Beispiel einer einfachen asynchronen Funktion:

\n\n
use tokio::time::{sleep, Duration};\n\n#[tokio::main]\nasync fn main() {\n    let task1 = async_task("Aufgabe 1");\n    let task2 = async_task("Aufgabe 2");\n\n    tokio::join!(task1, task2);\n}\n\nasync fn async_task(name: &str) {\n    println!("{} gestartet", name);\n    sleep(Duration::from_secs(1)).await;\n    println!("{} abgeschlossen", name);\n}
\n\n

Die Bibliothek tokio ist der Standard für asynchrones Rust. Sie bietet einen multithreaded Scheduler (work-stealing scheduler), der Aufgaben effizient auf CPU-Kerne verteilt.

\n\n

2. Traits Send und Sync: Parallelitätssicherheit

\n\n

Zwei Schlüssel-Traits gewährleisten die Parallelitätssicherheit in Rust:

\n\n
    \n
  • Send — Ein Typ kann sicher zwischen Threads übertragen werden.
  • \n
  • Sync — Auf einen Typ kann sicher von mehreren Threads gleichzeitig zugegriffen werden.
  • \n
\n\n

Die meisten Rust-Primitive implementieren diese Traits automatisch, wenn ihre Komponenten sie implementieren. Beispielsweise sind Arc<Mutex<i32>> Send und Sync, während Rc<RefCell<i32>> dies nicht ist.

\n\n

Betrachten wir ein Beispiel, das aufgrund einer Verletzung von Send nicht kompiliert wird:

\n\n
use std::rc::Rc;\nuse std::thread;\n\nlet data = Rc::new(5);\n\nthread::spawn(move || {\n    println!("{}", data);\n}); // Fehler: Rc<i32> implementiert Send nicht
\n\n

Der Rust-Compiler gibt eine klare Fehlermeldung aus und verhindert so Datenrennen bereits zur Kompilierzeit. Dies ist einer der Hauptvorteile der Sprache.

\n\n

3. Fortgeschrittene Synchronisation: Kanäle, Mutexe und RwLock

\n\n

Für den Datenaustausch zwischen Threads bietet Rust mehrere Primitive. Betrachten wir die leistungsfähigsten.

\n\n

Kanäle (mpsc)

\n\n

Multithreaded-Kanäle (multi-producer, single-consumer) ermöglichen das Senden von Nachrichten von mehreren Threads an einen einzelnen Empfänger:

\n\n
use std::sync::mpsc;\nuse std::thread;\n\nlet (tx, rx) = mpsc::channel();\n\nfor i in 0..3 {\n    let tx_clone = tx.clone();\n    thread::spawn(move || {\n        tx_clone.send(i).unwrap();\n    });\n}\n\ndrop(tx); // Sender schließen\n\nfor received in rx {\n    println!("Empfangen: {}", received);\n}
\n\n

Kanäle sind ideal für die "Producer-Consumer"-Architektur.

Blogs

Buchempfehlungen