Danh sách bài viết

Bài 229: Lỗi 'may outlive borrowed value' — Fix

Bài 229 của series Rust Cơ Bản — bài cuối Nhóm 28 Concurrency Threads, tiếp theo sang Nhóm 29 Concurrency Shared State. Bài tập trung vào một trong những lỗi compile kinh điển khi mới làm thread trong Rust: error[E0373]: closure may outlive the current function, but it borrows .... Bài học gồm: ví dụ tối thiểu cho thread::spawn(|| { use(&v); }) gây ra lỗi, paste nguyên văn rustc message để bạn đọc quen, giải thích vì sao compiler reject (thread::spawn yêu cầu closure 'static + Send, mà closure borrow biến local thì lifetime ngắn hơn 'static), và ba hướng fix tiêu biểu: (1) move keyword chuyển ownership v vào thread — đơn giản nhất khi caller không còn dùng v, (2) thread::scope ổn định từ Rust 1.63 đảm bảo thread join trước khi scope end nên cho phép borrow local — đẹp cho parallel compute, (3) Arc::new(v) + Arc::clone để chia sẻ data immutable qua nhiều thread sống lâu. Cuối bài có decision guide chọn fix nào theo tình huống, bài tập, và tổng kết toàn Nhóm 28 trước khi bước sang Nhóm 29 về shared mutable state.

09/06/2026
9 phút đọc
1 lượt xem
1

Mục Tiêu Bài Học

Sau bài học, bạn sẽ:

  • Nhận diện được lỗi error[E0373]: closure may outlive the current function, but it borrows ... khi thread::spawn giữ reference vào biến local.
  • Biết đọc rustc message từng dòng để hiểu ngay nguyên nhân — không cần search Google, vì compiler đã chỉ rõ cả chỗ sai lẫn gợi ý fix.
  • Giải thích vì sao thread::spawn ép closure 'static + Send: thread có thể sống vô thời hạn, compiler không thể chứng minh reference vào local còn valid khi thread đọc.
  • Sử dụng ba pattern fix tiêu biểu: move keyword chuyển ownership, thread::scope cho borrow non-'static, và Arc::new + Arc::clone để share data nhiều thread sống lâu.
  • Áp dụng decision guide chọn pattern phù hợp với từng tình huống thay vì "spam Arc mọi nơi".
  • Tổng kết kiến thức Nhóm 28 (Bài 222-229) trước khi sang Nhóm 29 — Shared State.
2

Lỗi Thường Gặp Khi spawn

Code "tự nhiên nhất" mà người mới hay viết — và compiler từ chối thẳng:

use std::thread;

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    let handle = thread::spawn(|| {
        // ❌ closure ngầm borrow `&v`
        println!("from thread: {:?}", v);
    });

    handle.join().unwrap();
}

Trên giấy nhìn rất hợp lý: v còn sống ở main, ta chỉ in nó trong thread thôi mà. Nhưng rustc không cho qua. Lỗi cụ thể là error[E0373]: closure may outlive the current function, but it borrows v, which is owned by the current function. Tên lỗi gợi đúng vấn đề: closure trong spawn có thể sống lâu hơn hàm main (về mặt lý thuyết — thread chạy độc lập), nên việc nó borrow một biến do main sở hữu là không an toàn.

Trường hợp gây nhầm lẫn hơn: ngay cả khi bạn join() ngay lập tức như ví dụ trên (thực tế thread không thể sống lâu hơn main), compiler vẫn không kiểm tra được điều đó từ signature của thread::spawn. Borrow checker chỉ nhìn vào type bound, không phải runtime behavior. Vậy giải pháp là gì? Phải hoặc chuyển ownership vào thread, hoặc dùng một API ràng buộc lifetime cho compiler thấy — đó là ba fix chúng ta sẽ học.

3

Rustc Message Đầy Đủ

Đây là output khi cargo build, đọc kỹ giúp fix nhanh:

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

Cấu trúc message rất giàu thông tin, hãy phân tích từng phần:

  • Tên lỗi E0373 — tra rustc --explain E0373 hoặc trang error code để xem ví dụ mở rộng.
  • Câu mô tả: "closure may outlive the current function, but it borrows v" — chỉ rõ closure (dòng 6) đang borrow biến v của caller.
  • note: "function requires argument type to outlive 'static" — đây là chìa khoá. thread::spawn yêu cầu argument có type bound 'static.
  • help: gợi ý cụ thể "use the move keyword", kèm diff minh hoạ thêm ++++ ở vị trí cần sửa. Trong nhiều trường hợp, đọc 3 dòng help + suggestion là đủ fix mà không cần Google.

Tip: bật cargo clippy trong quá trình code — đôi khi clippy còn báo sớm hơn rustc với gợi ý move closure hợp ngữ cảnh.

4

Vì Sao Compile Reject

Signature rút gọn của thread::spawn:

pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
    F: FnOnce() -> T + Send + 'static,
    T: Send + 'static,

Hai bound quan trọng:

  • Send: closure (và dữ liệu nó capture) phải chuyển an toàn qua thread boundary. Hầu hết type owned đều là Send, ngoại trừ một số ngoại lệ như Rc, *mut T.
  • 'static: closure không được giữ reference có lifetime ngắn hơn 'static. Tức là: hoặc closure không borrow gì từ ngoài, hoặc thứ nó borrow phải là static data (chữ ký hằng số, static variable).

Khi closure || { use(&v); } tự động capture v bằng reference (vì println! chỉ cần đọc), reference này có lifetime của stack frame main — ngắn hơn 'static. Vi phạm bound, compiler reject.

Vì sao Rust phải bảo thủ vậy? Lý do an toàn bộ nhớ: thread::spawn trả handle ngay, thread chạy nền độc lập. Không có gì trong type system đảm bảo caller phải join trước khi v bị drop. Nếu compiler cho phép borrow, kịch bản "caller return sớm → v drop → thread vẫn cầm reference vào memory đã free" là dangling reference chắc chắn — Rust hứa không có chuyện đó ở safe code.

Ba fix sau đây giải bài toán bằng ba góc khác nhau: thay borrow bằng move (Fix 1), dùng API ràng buộc lifetime cho compiler thấy (Fix 2), hoặc thay borrow bằng shared ownership qua refcount (Fix 3).

5

Fix 1: move Keyword

Đơn giản nhất, đúng như rustc gợi ý:

use std::thread;

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    let handle = thread::spawn(move || {
        // `v` đã được move vào closure, sống cùng thread
        println!("from thread: {:?}", v);
    });

    handle.join().unwrap();
    // ❌ println!("{:?}", v); // v không còn ở main nữa
}

Thêm move trước || ép closure capture by move thay vì by reference. v chuyển hẳn quyền sở hữu vào closure, sống cùng thread cho tới khi thread end. Lifetime của v giờ bound bởi closure → closure không còn giữ reference nào ngắn hơn 'static → thoả bound, compile pass.

Chi tiết về move closure xem Bài 224. Hạn chế của fix này: main không dùng v được nữa. Nếu bạn cần nhiều thread cùng đọc v hoặc main cũng cần dùng tiếp v, fix 1 không đủ — qua fix 2 hoặc fix 3.

6

Fix 2: thread::scope

std::thread::scope ổn định từ Rust 1.63 (8/2022) là API "phá vướng mắc 'static": scope đảm bảo mọi thread join trước khi scope end, nên compiler chấp nhận closure borrow biến local.

use std::thread;

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    thread::scope(|s| {
        s.spawn(|| {
            // ✅ borrow `&v` — không cần move
            println!("A: {:?}", v);
        });

        s.spawn(|| {
            println!("B sum = {}", v.iter().sum::<i32>());
        });
    }); // cả hai thread join ở đây

    // `v` vẫn còn — main dùng tiếp được
    println!("main: {:?}", v);
}

Ưu điểm so với fix 1:

  • Borrow trực tiếp &v — không tốn clone, không tốn Arc.
  • Nhiều thread cùng đọc cùng v không vấn đề.
  • Sau scope, v vẫn ở caller.

Hạn chế: scope chặn đợi đến khi tất cả thread con join — không phù hợp với background worker, daemon, hay thread cần trả handle cho caller bên ngoài scope. Chi tiết Bài 225.

7

Fix 3: Share Qua Arc

Khi cần nhiều thread sống lâu cùng đọc cùng data (background workers, server handler), thread::scope không hợp. Lúc đó pattern chuẩn là Arc<T> — atomic reference count cho phép nhiều owner cùng giữ data:

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

fn main() {
    let v = vec![1, 2, 3, 4, 5];
    let arc_v = Arc::new(v);

    let mut handles = Vec::new();
    for i in 0..3 {
        let arc_clone = Arc::clone(&arc_v); // chỉ bump refcount, không clone data
        let h = thread::spawn(move || {
            // `arc_clone` đã move vào closure (Arc là Send + 'static khi T là Send + Sync)
            println!("thread {} sees: {:?}", i, &arc_clone);
        });
        handles.push(h);
    }

    for h in handles {
        h.join().unwrap();
    }
    // arc_v vẫn còn ở main, refcount giảm khi các thread end
    println!("main refcount = {}", Arc::strong_count(&arc_v));
}

Ý tưởng: Arc::new(v) bọc v vào một heap allocation kèm atomic counter. Mỗi Arc::clone chỉ tăng counter (rất rẻ, một atomic add), không deep-clone v. Mỗi thread giữ một bản Arc sở hữu — bản này 'static (không reference vào stack), nên thoả bound của spawn sau khi move vào closure.

Lưu ý: Arc<T> chỉ cho phép đọc T chia sẻ giữa các thread (immutable). Nếu cần ghi từ nhiều thread, bọc thêm MutexArc<Mutex<T>> — chi tiết trong Nhóm 29 ngay sau đây.

8

Decision Guide

Đừng "spam Arc mọi nơi". Chọn fix theo tình huống:

| Fix               | Khi nào dùng                                         | Trade-off                          |
|-------------------|------------------------------------------------------|------------------------------------|
| 1. move           | Data dùng một lần trong một thread; caller xong việc | Caller mất quyền dùng data         |
| 2. thread::scope  | Parallel compute trên data local, đợi xong rồi tiếp  | Bị chặn cuối scope; không background|
| 3. Arc::clone     | Nhiều thread sống lâu cùng đọc data immutable        | Atomic refcount cost; chỉ đọc      |

Diễn giải nhanh:

  • move (single-use): phổ biến nhất cho hand-off đơn giản — "đẩy task cho thread, main đi làm việc khác". Ví dụ: spawn một worker xử lý job rồi kết thúc.
  • scope (parallel compute trên local): chia Vec thành chunks và compute song song, không muốn copy data. Pattern map-reduce gọn không cần dependency.
  • Arc (share long-running): nhiều thread server, mỗi thread cần đọc config/cache chung suốt vòng đời chương trình. Refcount đảm bảo data sống đủ lâu cho tất cả.

Một flow tay vịn: hỏi "Caller còn cần data sau khi spawn không?" Không → fix 1 move. Có và "Có thể đợi thread join trong cùng block không?" → fix 2 scope. Có nhưng "Cần thread sống ngoài scope" → fix 3 Arc.

9

Tổng Kết Nhóm 28

  • B222 thread::spawn + JoinHandle — mô hình OS thread cơ bản, join().unwrap() đợi.
  • B223 thread::sleep, park, unpark — synchronization primitive low-level.
  • B224 move closure trong spawn — chuyển ownership vào thread.
  • B225 thread::scope (Rust 1.63+) — borrow non-'static trong scope.
  • B226 Panic propagate — join().unwrap_err(), catch_unwind, scope auto-propagate.
  • B227 thread::Builder — name, stack_size cho debug/profile.
  • B228 Thread Local Storage — thread_local! macro, RefCell per-thread.
  • B229 (bài này) — fix lỗi may outlive borrowed value với move / scope / Arc.

Sau Nhóm 28, bạn đã có công cụ để spawn thread, đồng bộ join, share read-only data. Nhưng share mutable giữa các thread — counter, queue, cache — là chủ đề riêng cần Mutex, RwLock, atomic. Đó chính là Nhóm 29.

10

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. Viết một chương trình spawn 3 thread cùng in một String, chọn fix phù hợp và giải thích lý do.
  2. thread::spawn yêu cầu hai bound nào trên closure? Vì sao mỗi bound cần thiết?
  3. So sánh chi phí: v.clone() trước khi move vs Arc::new(v) + Arc::clone cho 4 thread đọc một Vec<i64> 1 triệu phần tử. Bên nào tốn bộ nhớ hơn?
  4. Khi nào thread::scope không thể thay thread::spawn + Arc?
  5. Đọc lại rustc message ở Section 3 và chỉ ra: tên error code, dòng nào gợi ý fix, fix gợi ý là gì?
11

Bài Tiếp Theo

Bài 230: Send & Sync — 2 Marker Trait Quan Trọng — mở đầu Nhóm 29 (Concurrency - Shared State). Bài giải thích hai marker trait tự động derive mà compiler dùng để quyết "type này có an toàn qua thread không": Send (chuyển ownership) và Sync (chia sẻ reference). Hiểu hai trait này là nền cho mọi pattern shared state phía sau: Arc, Mutex, RwLock, atomic.