Mục lục
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 ...khithread::spawngiữ 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:
movekeyword chuyển ownership,thread::scopecho 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
Arcmọ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.
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.
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— trarustc --explain E0373hoặ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ếnvcủa caller. - note: "function requires argument type to outlive
'static" — đây là chìa khoá.thread::spawnyêu cầu argument có type bound'static. - help: gợi ý cụ thể "use the
movekeyword", 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.
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ố,staticvariable).
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).
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.
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ốnclone, không tốnArc. - Nhiều thread cùng đọc cùng
vkhông vấn đề. - Sau scope,
vvẫ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.
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 là '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 Mutex → Arc<Mutex<T>> — chi tiết trong Nhóm 29 ngay sau đây.
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
Vecthà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.
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
moveclosure trongspawn— chuyển ownership vào thread. - B225
thread::scope(Rust 1.63+) — borrow non-'statictrong 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,RefCellper-thread. - B229 (bài này) — fix lỗi
may outlive borrowed valuevớ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.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- 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. thread::spawnyêu cầu hai bound nào trên closure? Vì sao mỗi bound cần thiết?- So sánh chi phí:
v.clone()trước khimovevsArc::new(v)+Arc::clonecho 4 thread đọc mộtVec<i64>1 triệu phần tử. Bên nào tốn bộ nhớ hơn? - Khi nào
thread::scopekhông thể thaythread::spawn+Arc? - Đọ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ì?
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.
