Zum Inhalt springen

Rust-Fehler E0373: closure may outlive the current function (move fehlt)

E0373 bedeutet: Ein Closure (eine anonyme Funktion) leiht sich eine lokale Variable, aber das Closure könnte noch laufen (oder noch existieren), wenn die Funktion, der diese Variable gehört, schon fertig ist. Meistens triffst du den Fehler, wenn du zum ersten Mal thread::spawn aufrufst oder ein Closure aus einer Funktion zurückgibst. Die gute Nachricht: Der Compiler verrät dir die Lösung schon selbst, und sie ist fast immer ein einziges Wort – move. Diese Seite zeigt dir zuerst die Lösung und erklärt dann, warum. Die anderen Fehler dieser Reihe findest du unter Rust-Compilerfehler.

error[E0373]: closure may outlive the current function, but it borrows `scores`, which is owned by the current function
--> src/main.rs:5:32
|
5 | let handle = thread::spawn(|| {
| ^^ may outlive borrowed value `scores`
6 | println!("Scores: {scores:?}"); // ❌ leiht sich scores nur aus
| ------ `scores` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src/main.rs:5:18
|
5 | let handle = thread::spawn(|| {
| __________________^
6 | | println!("Scores: {scores:?}"); // ❌ leiht sich scores nur aus
7 | | });
| |______^
help: to force the closure to take ownership of `scores` (and any other referenced variables), use the `move` keyword
|
5 | let handle = thread::spawn(move || {
| ++++

Auf Deutsch heißt das ungefähr: „Das Closure lebt vielleicht länger als die aktuelle Funktion, aber es leiht sich scores, und scores gehört der aktuellen Funktion.“ Dieses Programm erzeugt die Meldung:

use std::thread;
fn main() {
let scores = vec![90, 85, 77];
let handle = thread::spawn(|| {
println!("Scores: {scores:?}"); // ❌ leiht sich scores nur aus
});
handle.join().unwrap();
}

Bei async-Code siehst du denselben Fehlercode mit leicht anderen Worten: „async block may outlive the current function, but it borrows user_id“. Es ist derselbe Fehler mit derselben Lösung (async move).

Lies die Labels von oben nach unten:

  1. ^^ may outlive borrowed value zeigt auf das Closure ||. Rust kann nicht versprechen, dass das Closure fertig ist, bevor main weitermacht.
  2. `scores` is borrowed here zeigt auf die Stelle, an der das Closure scores benutzt. Weil println! den Vektor nur lesen muss, hat das Closure eine Referenz auf scores eingefangen, nicht den Vektor selbst.
  3. function requires argument type to outlive 'static erklärt, warum das ein Problem ist: thread::spawn nimmt nur Closures an, die so lange leben können, wie das Programm sie braucht. 'static heißt hier: „leiht sich nichts, was verschwinden könnte“.

Stell dir vor, du leihst einem Freund dein Notizbuch und sagst „bring es zurück, wenn du fertig bist“ – während du gerade in eine andere Stadt umziehst. Wenn dein Freund langsam arbeitet, hat das Notizbuch kein Zuhause mehr, zu dem es zurückkehren kann. Rust lehnt diesen Deal ab und bittet dich, das Notizbuch stattdessen zu verschenken.

Ein Closure fängt Variablen auf die billigste Art ein, die funktioniert. Liest es eine Variable nur, leiht es sie sich mit &. Das ist ideal für Closures, die sofort laufen, etwa die, die du an .map() oder .filter() übergibst. Mehr dazu findest du auf der Seite Closures.

Ein neuer Thread ist anders. Er kann beliebig lange laufen, und main wartet nicht auf ihn, solange du nicht join aufrufst. Hätte der Thread eine Referenz auf scores, und main wäre zuerst fertig, würde scores freigegeben, und der Thread würde freigegebenen Speicher lesen. Das ist eine „hängende Referenz“ (Dangling Reference), einer der häufigsten Fehler in Thread-Code in C und C++. Rust verhindert ihn schon beim Kompilieren.

Dasselbe passiert, wenn du ein Closure zurückgibst. Die lokalen Variablen der Funktion (und ihre Parameter) werden beim Zurückkehren freigegeben. Ein Closure, das sie sich noch leiht, würde ins Leere zeigen.

Lösung 1: move hinzufügen (die übliche Antwort)

Abschnitt betitelt „Lösung 1: move hinzufügen (die übliche Antwort)“

Nimm diese Lösung fast immer. Genau das schlägt rustc vor. move sagt dem Closure, dass es die Variablen, die es benutzt, besitzen soll (Ownership). So reisen sie mit ihm:

use std::thread;
fn main() {
let scores = vec![90, 85, 77];
let handle = thread::spawn(move || {
let total: i32 = scores.iter().sum();
println!("Punkte: {scores:?}, Summe {total}");
});
handle.join().unwrap();
}

Ausgabe:

Punkte: [90, 85, 77], Summe 252
▶ Selbst ausprobieren

Dieselbe Lösung funktioniert für eine Funktion, die ein Closure zurückgibt. Ohne move leiht sich |x| x + n den Parameter n – und der ist weg, sobald make_adder zurückkehrt. Mit move behält jedes Closure seine eigene Kopie von n:

fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
fn main() {
let add_five = make_adder(5);
let add_ten = make_adder(10);
println!("{}", add_five(1));
println!("{}", add_ten(1));
}

Ausgabe:

6
11
▶ Selbst ausprobieren

move ändert nicht, welchen Closure-Trait du bekommst. Ein move-Closure, das seine Daten nur liest, ist weiterhin Fn und lässt sich beliebig oft aufrufen.

Lösung 2: Vorher klonen, wenn du den Wert noch brauchst

Abschnitt betitelt „Lösung 2: Vorher klonen, wenn du den Wert noch brauchst“

Nach move gehört die Variable dem Closure. Benutzt main sie danach noch, bekommst du E0382 („borrow of moved value“), und rustc schlägt vor, den Wert vor dem Verschieben zu klonen. Mach eine Kopie für das Closure und behalte das Original:

use std::thread;
fn main() {
let guests = vec![String::from("Ana"), String::from("Ben")];
let guests_for_thread = guests.clone();
let handle = thread::spawn(move || {
println!("Der Thread sieht {} Gäste", guests_for_thread.len());
});
handle.join().unwrap();
println!("main hat noch: {guests:?}");
}

Ausgabe:

Der Thread sieht 2 Gäste
main hat noch: ["Ana", "Ben"]
▶ Selbst ausprobieren

Eine kleine Liste zu klonen ist billig und völlig in Ordnung. Sind die Daten groß, oder brauchen viele Threads sie, nimm Arc (Lösung 3). Dann teilen sich alle eine einzige Kopie.

Lösung 3: Mit Arc zwischen mehreren Threads teilen

Abschnitt betitelt „Lösung 3: Mit Arc zwischen mehreren Threads teilen“

Nimm diese Lösung, wenn mehrere Threads dieselben Daten lesen müssen. Arc („atomically reference counted“, atomar referenzgezählt) lässt viele Besitzer einen Wert teilen. Arc::clone kopiert nur einen Zeiger und erhöht einen Zähler; die Daten selbst werden nicht kopiert:

use std::sync::Arc;
use std::thread;
fn main() {
let prices = Arc::new(vec![4.5, 2.0, 3.5]);
let mut handles = Vec::new();
for discount in [0.0, 0.1, 0.5] {
let prices = Arc::clone(&prices); // ein Handle pro Thread
handles.push(thread::spawn(move || {
let total: f64 = prices.iter().sum();
total * (1.0 - discount)
}));
}
for handle in handles {
println!("Summe: {:.2}", handle.join().unwrap());
}
println!("Preise: {prices:?}");
}

Ausgabe:

Summe: 10.00
Summe: 9.00
Summe: 5.00
Preise: [4.5, 2.0, 3.5]
▶ Selbst ausprobieren

Arc allein erlaubt nur gemeinsames Lesen. Müssen die Threads die Daten auch ändern, verpack sie als Arc<Mutex<T>>. Siehe Nebenläufigkeit und Geteilter Zustand.

Lösung 4: Sicher ausleihen mit std::thread::scope

Abschnitt betitelt „Lösung 4: Sicher ausleihen mit std::thread::scope“

Nimm diese Lösung, wenn die Threads die Daten nur für eine kurze Aufgabe brauchen und du lieber ausleihen als verschieben oder klonen willst. Threads, die innerhalb von thread::scope gestartet werden, sind garantiert fertig, bevor scope zurückkehrt. Lokale Variablen auszuleihen ist dann sicher, und du brauchst kein move:

use std::thread;
fn main() {
let scores = vec![90, 85, 77, 64];
let (first, second) = scores.split_at(2);
let (a, b) = thread::scope(|s| {
let h1 = s.spawn(|| first.iter().sum::<i32>());
let h2 = s.spawn(|| second.iter().sum::<i32>());
(h1.join().unwrap(), h2.join().unwrap())
}); // hier sind beide Threads fertig
println!("{a} + {b} = {}", a + b);
println!("Gehört noch uns: {scores:?}");
}

Ausgabe:

175 + 141 = 316
Gehört noch uns: [90, 85, 77, 64]
▶ Selbst ausprobieren

Scoped Threads sind die sauberste Antwort für „Arbeit aufteilen, dann die Ergebnisse benutzen“. Die Lektion Geteilter Zustand beginnt mit ihnen.

Threads in einer Schleife starten. Sogar eine einfache Zahl wird ausgeliehen, wenn das Closure sie nur liest:

for worker in 1..=3 {
handles.push(thread::spawn(|| {
println!("Worker {worker} started"); // ❌ leiht sich `worker` aus
}));
}

Lösung: thread::spawn(move || ...). worker ist ein i32, also kopiert move den Wert einfach in jeden Thread.

Ein Closure in einer Box zurückgeben. Box::new(|| greeting.clone()) in einer Funktion, die Box<dyn Fn() -> String> zurückgibt, leiht sich die lokale Variable greeting, und rustc ergänzt note: closure is returned here (das Closure wird hier zurückgegeben). Lösung: Box::new(move || greeting.clone()). Mehr dazu in Fn, FnMut und FnOnce.

Async-Tasks (Tokio und Co.). tokio::spawn funktioniert wie thread::spawn: Der Task läuft vielleicht noch lange, nachdem die aktuelle Funktion zurückgekehrt ist, also braucht er 'static-Daten. Schreibst du tokio::spawn(async { ... }) und benutzt darin eine lokale Variable, bekommst du „async block may outlive the current function“. Lösung: tokio::spawn(async move { ... }), und alles, was du außerhalb des Tasks noch brauchst, vorher mit clone oder Arc::clone kopieren. Siehe Async/await.

Kurzer Check

1. Ein Thread-Closure gibt name aus, ohne move zu benutzen. Warum bekommst du E0373?

2. Du fügst move hinzu, aber main muss den Vektor nach dem Start des Threads noch ausgeben. Was ist die einfachste Lösung?

3. Mit welchem Werkzeug können Threads lokale Variablen ohne move ausleihen?

Punkte: 0 / 3