Bỏ qua để đến nội dung

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.

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 scores
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 || {
| ++++

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).

Đọc các nhãn từ trên xuống dưới:

  1. ^^ may outlive borrowed value chỉ vào closure ||. Rust không thể hứa rằng closure sẽ xong việc trước khi main đi tiếp.
  2. `scores` is borrowed here chỉ vào chỗ closure dùng scores. Vì println! chỉ cần đọc, closure đã bắt một tham chiếu tới scores, chứ không phải chính vector đó.
  3. function requires argument type to outlive 'static giải thích vì sao đó là vấn đề: thread::spawn chỉ nhận những closure có thể sống lâu bao nhiêu tùy chương trình cần. 'static nghĩ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ổ.

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 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 252
▶ Tự chạy thử

Cá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ả:

6
11
▶ Tự chạy thử

move 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ách
main vẫn còn: ["Ana", "Ben"]
▶ Tự chạy thử

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.00
Tổng: 9.00
Tổng: 5.00
Giá: [4.5, 2.0, 3.5]
▶ Tự chạy thử

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 = 316
Vẫn là của chúng ta: [90, 85, 77, 64]
▶ Tự chạy thử

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.

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.

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