Parallelität und Multithreading in Rust: Von A bis Z
Moderne Anwendungen erfordern zunehmend die effiziente Nutzung von Mehrkernprozessoren. Rust bietet einen einzigartigen Ansatz für Parallelität und Multithreading, der hohe Leistung mit Speichersicherheitsgarantien zur Kompilierzeit kombiniert. In diesem Artikel behandeln wir die wichtigsten Konzepte: von der Thread-Erstellung bis zur Verwendung von Mutexen und Kanälen, mit praktischen Codebeispielen.
1. Grundlagen des Multithreadings: Threads erstellen und verwalten
In Rust werden Threads mit der Standardbibliothek std::thread erstellt. Jeder Thread wird unabhängig ausgeführt, kann aber Daten mit anderen Threads austauschen.
Ein einfaches Beispiel zur Thread-Erstellung:
use std::thread;use std::time::Duration;
fn main() { let handle = thread::spawn(|| { for i in 1..10 { println!("Thread: Zahl {}", i); thread::sleep(Duration::from_millis(1)); } });
for i in 1..5 { println!("Hauptthread: Zahl {}", i); thread::sleep(Duration::from_millis(1)); }
handle.join().unwrap(); // Warten auf das Ende des Threads}Wichtige Punkte:
thread::spawnnimmt einen Closure entgegen, der in einem neuen Thread ausgeführt wird.- Die Methode
joinblockiert den aktuellen Thread, bis der angegebene Thread beendet ist. - Ohne
joinkönnte der Hauptthread vor dem Kindthread enden.
Für die Datenübergabe zwischen Threads verwenden Sie Closures mit move:
use std::thread;
fn main() { let data = vec![1, 2, 3, 4, 5];
let handle = thread::spawn(move || { println!("Daten aus dem Thread: {:?}", data); });
handle.join().unwrap(); // println!("{:?}", data); // Fehler! data wurde bereits verschoben}Das Schlüsselwort move überträgt den Besitz der Daten an den Closure und verhindert so Datenrennen zur Kompilierzeit.
2. Sicherer Datenaustausch: Mutexe und Sperren
Wenn mehrere Threads auf gemeinsame Daten zugreifen, besteht die Gefahr von Datenrennen. Rust löst dieses Problem mit Mutexen (Mutex) und atomaren Typen.
Beispiel für die Verwendung von Mutex mit Arc (atomarer Referenzzähler):
use std::sync::{Arc, Mutex};use std::thread;
fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![];
for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); }
for handle in handles { handle.join().unwrap(); }
println!("Ergebnis: {}", *counter.lock().unwrap()); // 10}Wichtige Details:
Arc<T>ermöglicht mehreren Threads den Besitz derselben Daten (atomares Klonen).Mutex::lock()gibt eine Sperre zurück; wenn die Sperre bereits belegt ist, wartet der Thread.- Die Sperre wird automatisch freigegeben, wenn der Gültigkeitsbereich verlassen wird.
Für eine feinere Kontrolle verwenden Sie RwLock, das Leser und Schreiber trennt:
use std::sync::RwLock;
let lock = RwLock::new(5);
// Lesen: mehrere Threads gleichzeitig{ let r1 = lock.read().unwrap(); let r2 = lock.read().unwrap(); println!("Lesen: {} und {}", r1, r2);}
// Schreiben: nur ein Thread{ let mut w = lock.write().unwrap(); *w += 1; println!("Geschrieben: {}", w);}3. Kanäle zur Nachrichtenübertragung
Eine Alternative zum gemeinsamen Speicher ist das Akteursmodell mit Kanälen. Rust bietet einen Mehrfachproduzenten-, Einfachkonsumenten-Kanal (mpsc).
Basisbeispiel:
u