Lỗi Rust E0373: closure may outlive the current function (closure sống lâu hơn hàm)
E0373 nghĩa là một closure mượn một biến cục bộ, nhưng closure đó có thể vẫn chạy (hoặc vẫn tồn tại) sau khi hàm sở hữu biến đã kết thúc. Bạn thường gặp nó lần đầu khi gọi thread::spawn hoặc trả về một closure từ một hàm. Tin vui là trình biên dịch đã nói sẵn cách sửa, và nó gần như luôn chỉ là một từ: move. Trang này cho bạn xem cách sửa trước, rồi giải thích vì sao. Các lỗi khác trong loạt bài này nằm ở lỗi trình biên dịch Rust.
Thông báo lỗi
Phần tiêu đề “Thông báo lỗi”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:?}"); // ❌ only borrows scores | ------ `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:?}"); // ❌ only borrows scores7 | | }); | |______^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 || { | ++++Trình biên dịch báo rằng closure có thể sống lâu hơn hàm hiện tại trong khi nó chỉ mượn scores, và gợi ý thêm từ khóa move.
Đây là chương trình gây ra lỗi:
use std::thread;
fn main() { let scores = vec![90, 85, 77]; let handle = thread::spawn(|| { println!("Scores: {scores:?}"); // ❌ only borrows scores }); handle.join().unwrap();}Với code async, bạn sẽ thấy cùng mã lỗi với câu chữ hơi khác: “async block may outlive the current function, but it borrows user_id”. Đó vẫn là lỗi này, và cách sửa cũng giống vậy (async move).
Ý nghĩa
Phần tiêu đề “Ý nghĩa”Đọc các nhãn từ trên xuống dưới:
^^ may outlive borrowed valuechỉ vào closure||. Rust không thể hứa rằng closure sẽ xong việc trước khimainđi tiếp.`scores` is borrowed herechỉ vào chỗ closure dùngscores. Vìprintln!chỉ cần đọc, closure đã bắt một tham chiếu tớiscores, chứ không phải chính vector đó.function requires argument type to outlive 'staticgiải thích vì sao đó là vấn đề:thread::spawnchỉ nhận những closure có thể sống lâu bao nhiêu tùy chương trình cần.'staticnghĩa là “không mượn thứ gì có thể biến mất”.
Hãy tưởng tượng bạn cho một người bạn mượn cuốn sổ và dặn “xong thì trả lại nhé”, trong khi bạn sắp chuyển sang thành phố khác. Nếu bạn ấy làm chậm, cuốn sổ sẽ không còn chỗ nào để trả về. Rust từ chối thỏa thuận đó và bảo bạn hãy tặng luôn cuốn sổ.
Vì sao Rust làm vậy
Phần tiêu đề “Vì sao Rust làm vậy”Closure bắt biến theo cách rẻ nhất mà vẫn chạy được. Nếu nó chỉ đọc một biến, nó mượn biến đó bằng &. Điều này rất tốt với các closure chạy ngay lập tức, như những closure bạn truyền cho .map() hay .filter(). Bạn có thể đọc thêm ở trang Closure.
Một luồng (thread) mới thì khác. Nó có thể chạy bao lâu cũng được, và main không chờ nó trừ khi bạn gọi join. Nếu luồng giữ một tham chiếu tới scores, và main kết thúc trước, scores sẽ bị drop và luồng sẽ đọc vùng nhớ đã giải phóng. Đó là một “tham chiếu treo” (dangling reference), một trong những lỗi phổ biến nhất trong code đa luồng C và C++. Rust chặn nó ngay lúc biên dịch.
Điều tương tự xảy ra khi bạn trả về một closure. Các biến cục bộ của hàm (và các tham số của nó) bị drop khi hàm trả về. Một closure vẫn mượn chúng sẽ trỏ vào hư không.
Cách sửa
Phần tiêu đề “Cách sửa”Cách sửa 1: Thêm move (câu trả lời thường gặp)
Phần tiêu đề “Cách sửa 1: Thêm move (câu trả lời thường gặp)”Chọn cách này gần như mọi lúc. Đó chính là điều rustc gợi ý. move bảo closure lấy quyền sở hữu (ownership) các biến nó dùng, để chúng đi theo closure:
use std::thread;
fn main() { let scores = vec![90, 85, 77]; let handle = thread::spawn(move || { let total: i32 = scores.iter().sum(); println!("Điểm: {scores:?}, tổng {total}"); }); handle.join().unwrap();}Kết quả:
Điểm: [90, 85, 77], tổng 252Cách sửa này cũng dùng được cho một hàm trả về closure. Không có move, |x| x + n mượn tham số n, vốn biến mất ngay khi make_adder trả về. Có move, mỗi closure giữ bản sao n của riêng 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));}Kết quả:
611move không thay đổi trait closure mà bạn nhận được. Một closure move chỉ đọc dữ liệu của nó vẫn là Fn và có thể gọi nhiều lần.
Cách sửa 2: Clone trước, khi bạn vẫn cần giá trị
Phần tiêu đề “Cách sửa 2: Clone trước, khi bạn vẫn cần giá trị”Sau move, biến thuộc về closure. Nếu main dùng nó về sau, bạn sẽ gặp E0382 (“borrow of moved value”), và rustc gợi ý clone giá trị trước khi move. Hãy tạo một bản sao cho closure và giữ lại bản gốc:
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!("Luồng thấy {} khách", guests_for_thread.len()); }); handle.join().unwrap();
println!("main vẫn còn: {guests:?}");}Kết quả:
Luồng thấy 2 kháchmain vẫn còn: ["Ana", "Ben"]Clone một danh sách nhỏ rất rẻ và hoàn toàn ổn. Nếu dữ liệu lớn, hoặc nhiều luồng cùng cần nó, hãy dùng Arc (Cách sửa 3) để tất cả dùng chung một bản.
Cách sửa 3: Chia sẻ bằng Arc giữa nhiều luồng
Phần tiêu đề “Cách sửa 3: Chia sẻ bằng Arc giữa nhiều luồng”Chọn cách này khi nhiều luồng cần đọc cùng một dữ liệu. Arc (“atomically reference counted”, đếm tham chiếu nguyên tử) cho phép nhiều chủ sở hữu dùng chung một giá trị. Arc::clone chỉ sao chép một con trỏ và tăng một bộ đếm; bản thân dữ liệu không bị sao chép:
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); // mỗi luồng một handle handles.push(thread::spawn(move || { let total: f64 = prices.iter().sum(); total * (1.0 - discount) })); }
for handle in handles { println!("Tổng: {:.2}", handle.join().unwrap()); } println!("Giá: {prices:?}");}Kết quả:
Tổng: 10.00Tổng: 9.00Tổng: 5.00Giá: [4.5, 2.0, 3.5]Chỉ mình Arc thì chỉ cho phép chia sẻ để đọc. Nếu các luồng còn cần thay đổi dữ liệu, hãy bọc nó thành Arc<Mutex<T>>. Xem Đồng thời và Trạng thái dùng chung.
Cách sửa 4: Mượn an toàn với std::thread::scope
Phần tiêu đề “Cách sửa 4: Mượn an toàn với std::thread::scope”Chọn cách này khi các luồng chỉ cần dữ liệu cho một việc ngắn và bạn muốn mượn thay vì move hay clone. Các luồng được khởi chạy bên trong thread::scope được đảm bảo sẽ xong trước khi scope trả về, nên mượn biến cục bộ là an toàn và không cần 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()) }); // cả hai luồng đã xong ở đây
println!("{a} + {b} = {}", a + b); println!("Vẫn là của chúng ta: {scores:?}");}Kết quả:
175 + 141 = 316Vẫn là của chúng ta: [90, 85, 77, 64]Luồng có phạm vi (scoped thread) là lời giải gọn gàng nhất cho kiểu việc “chia việc ra, rồi dùng kết quả”. Bài Trạng thái dùng chung bắt đầu với chúng.
Thường gặp ở đâu
Phần tiêu đề “Thường gặp ở đâu”Tạo luồng trong vòng lặp. Ngay cả một con số bình thường cũng bị mượn nếu closure chỉ đọc nó:
for worker in 1..=3 { handles.push(thread::spawn(|| { println!("Worker {worker} started"); // ❌ borrows `worker` }));}Cách sửa: thread::spawn(move || ...). worker là một i32, nên move chỉ đơn giản sao chép nó vào từng luồng.
Trả về một closure được đóng hộp (boxed). Box::new(|| greeting.clone()) bên trong một hàm trả về Box<dyn Fn() -> String> sẽ mượn biến cục bộ greeting, và rustc thêm note: closure is returned here. Cách sửa: Box::new(move || greeting.clone()). Đọc thêm ở Fn, FnMut và FnOnce.
Tác vụ bất đồng bộ (Tokio và các thư viện tương tự). tokio::spawn hoạt động giống thread::spawn: tác vụ có thể chạy rất lâu sau khi hàm hiện tại trả về, nên nó cần dữ liệu 'static. Viết tokio::spawn(async { ... }) và dùng một biến cục bộ bên trong sẽ gây ra “async block may outlive the current function”. Cách sửa: tokio::spawn(async move { ... }), và clone hoặc Arc::clone những gì bạn vẫn cần bên ngoài tác vụ. Xem Async/await.
Lỗi liên quan
Phần tiêu đề “Lỗi liên quan”- E0382: borrow of moved value: xuất hiện ngay sau khi bạn thêm
moverồi lại dùng biến đó bên ngoài closure. Hãy clone trước (Cách sửa 2) hoặc dùngArc(Cách sửa 3). - E0597: borrowed value does not live long enough: họ hàng gần. Một tham chiếu vẫn còn được dùng sau khi giá trị nó trỏ tới đã bị drop.
- E0515: cannot return reference to local variable: cùng vấn đề “hàm trả về là nó biến mất”, nhưng với một tham chiếu bình thường thay vì closure.
- E0106: missing lifetime specifier: xuất hiện khi bạn cố trả về một tham chiếu (hoặc một kiểu closure) có mượn thứ gì đó.
- Tất cả lỗi trình biên dịch: danh sách đầy đủ các trang lỗi.
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. Closure của một luồng in name mà không dùng move. Vì sao bạn gặp E0373?
2. Bạn đã thêm move, nhưng main vẫn cần in vector sau khi tạo luồng. Cách sửa đơn giản nhất là gì?
3. Công cụ nào cho phép các luồng mượn biến cục bộ mà không cần move?
Điểm: 0 / 3
Đọc thêm
Phần tiêu đề “Đọc thêm”- E0373 — Rust error codes index (tiếng Anh): giải thích chính thức của lỗi này, kèm cách sửa bằng
move. - Using Threads to Run Code Simultaneously — The Rust Book (tiếng Anh): mục “Using move Closures with Threads” đi qua đúng lỗi này.
- Closures — The Rust Book (tiếng Anh): closure bắt biến bằng tham chiếu, bằng tham chiếu khả biến, hoặc bằng giá trị như thế nào.
- Từ khóa
move— tài liệu thư viện chuẩn (tiếng Anh):movelàm gì với những thứ closure bắt được, kèm ví dụ ngắn. std::thread::scope— tài liệu thư viện chuẩn (tiếng Anh): luồng có phạm vi, có thể mượn dữ liệu cục bộ một cách an toàn.