Cách xử lý lỗi Rust E0373: Khi Closure tồn tại lâu hơn hàm chứa nó

intermediate🦀 Rust2026-07-16| Rust (mọi phiên bản), thường gặp trên các môi trường Linux, macOS hoặc Windows khi sử dụng std::thread hoặc async runtimes như Tokio.

Error Message

error[E0373]: closure may outlive the current function, but it borrows `x`, which is owned by the current function
#rust#closure#move#ownership#concurrency#tokio

Vấn đề: Cuộc chiến giữa Ownership và LifetimesBạn đang xây dựng một ứng dụng đa luồng (concurrent), bạn chạy cargo run, và trình biên dịch đột nhiên chặn tiến trình của bạn với lỗi error[E0373]. Lỗi này là một "nghi thức trưởng thành" đối với các lập trình viên Rust. Nó thường xuất hiện khi bạn cố gắng truyền một biến cục bộ vào một thread mới hoặc một khối asynchronous. Trình biên dịch thực chất đang bảo vệ bạn khỏi một thảm họa về bộ nhớ.

Rust phải đảm bảo rằng bất kỳ dữ liệu nào mà thread của bạn sử dụng đều phải hợp lệ chừng nào thread đó còn chạy. Nếu hàm main kết thúc và giải phóng stack của nó trong khi một background thread vẫn đang cố gắng đọc một biến, bạn sẽ gặp lỗi use-after-free. Rust ngăn chặn điều này trước khi nó kịp xảy ra. Dưới đây là cấu trúc lỗi tiêu chuẩn:

error[E0373]: closure may outlive the current function, but it borrows `x`, which is owned by the current function
  --> src/main.rs:6:32
   |
6  |     thread::spawn(|| {
   |                    ^^ may outlive borrowed value `x`
7  |         println!("{}", x);
   |                        - `x` is borrowed here
   |
note: function requires argument type to outlive `'static`

Tại sao trình biên dịch lại lo ngạiThông thường, các closure cố gắng mượn (borrow) các biến từ môi trường của chúng bằng tham chiếu. Tuy nhiên, std::thread::spawn yêu cầu một closure có lifetime 'static. Điều này có nghĩa là closure phải có khả năng tồn tại trong suốt thời gian chạy của chương trình. Nếu hàm của bạn thoát sau 5 mili giây nhưng thread được tạo ra chạy trong 10 giây, tham chiếu đó tới x sẽ trở thành một con trỏ lơ lửng (dangling pointer). Trình kiểm tra mượn (borrow checker) của Rust nhận thấy sự không khớp này và từ chối biên dịch.

Các giải pháp hiệu quả### 1. Cách sửa nhanh nhất: Từ khóa 'move'Nếu hàm gốc không còn cần biến đó nữa, hãy sử dụng từ khóa move. Điều này bắt buộc closure phải nắm toàn quyền sở hữu (ownership) các biến mà nó captures. Nó thực sự di chuyển dữ liệu từ stack của hàm vào không gian bộ nhớ riêng của closure.

Mã nguồn bị lỗi:```

use std::thread;

fn main() { let message = String::from("Hello from the stack!");

let handle = thread::spawn(|| {
    println!("{}", message); // Lỗi: message chỉ được mượn
});

handle.join().unwrap();

}


#### Mã nguồn đã sửa:```
use std::thread;

fn main() {
    let message = String::from("Hello from the heap!");

    // 'move' chuyển quyền sở hữu sang thread mới
    let handle = thread::spawn(move || {
        println!("{}", message); 
    });

    handle.join().unwrap();
    // println!("{}", message); // Dòng này sẽ lỗi; message đã bị di chuyển!
}

2. Chia sẻ dữ liệu với ArcĐôi khi bạn cần truy cập cùng một dữ liệu trong cả hàm main và background thread. Vì một giá trị chỉ có thể có một chủ sở hữu (owner), một lệnh move đơn giản sẽ không hoạt động nếu bạn cần sử dụng biến đó sau này. Để giải quyết vấn đề này, hãy bọc dữ liệu của bạn trong một con trỏ Arc (Atomically Reference Counted). Điều này cho phép có nhiều chủ sở hữu bằng cách theo dõi số lượng tham chiếu đang tồn tại.

use std::thread;
use std::sync::Arc;

fn main() {
    let data = Arc::new(String::from("Shared config"));

    let data_clone = Arc::clone(&data);
    let handle = thread::spawn(move || {
        println!("Thread reads: {}", data_clone);
    });

    println!("Main reads: {}", data);
    handle.join().unwrap();
}

3. Giải quyết E0373 trong các khối AsyncCác runtime như Tokio hoặc async-std thường yêu cầu các biến được capture phải là 'static vì các task có thể được di chuyển giữa các thread. Bạn phải áp dụng async move để đảm bảo task mang theo dữ liệu của nó với nó.

use tokio;

#[tokio::main]
async fn main() {
    let request_id = String::from("REQ-123");

    tokio::spawn(async move {
        // Nếu không có 'move', request_id sẽ bị mượn và gây ra lỗi E0373
        println!("Processing: {}", request_id);
    }).await.unwrap();
}

Xác minh và kiểm traKiểm tra lại cách sửa của bạn với các bước sau:

  • Chạy Cargo Check: Cách này nhanh hơn việc build toàn bộ. Nếu lỗi biến mất, logic ownership của bạn đã hợp lệ.- Xác minh hành vi của Move: Nếu bạn đã sử dụng move, thử in biến đó ở cuối hàm main. Nếu trình biên dịch báo "use of moved value," bạn đã chuyển quyền sở hữu thành công.- Vị trí của Clone: Đảm bảo Arc::clone diễn ra trước closure move, chứ không phải bên trong nó. Việc clone bên trong closure sẽ chỉ clone một tham chiếu sắp hết hạn.## Mẹo chuyên nghiệp để quản lý Ownership sạch sẽ- Các kiểu Copy: Các kiểu như i32, f64, và bool thực thi trait Copy. Khi bạn move những kiểu này, Rust thực tế sẽ sao chép các bit. Biến gốc vẫn có thể sử dụng được trong hàm cha.- Tận dụng Scoped Threads: Nếu bạn đang dùng Rust 1.63 hoặc mới hơn, hãy sử dụng std::thread::scope. Các thread này được đảm bảo sẽ kết thúc trước khi scope kết thúc. Điều này cho phép bạn mượn các biến cục bộ một cách an toàn mà không cần Arc hoặc move.- Capture từng phần: Nếu bạn có một struct 100MB nhưng thread chỉ cần một trường string 20 byte, hãy trích xuất trường đó thành biến riêng trước. Chỉ move biến nhỏ đó vào closure để giữ cho dung lượng bộ nhớ nhẹ nhàng.

Related Error Notes