Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu
Arc<T>là smart pointer reference-counted phiên bản atomic — interface giốngRc<T>nhưng counter dùng atomic instruction nên an toàn khi nhiều thread cùng thao tác. - Dùng được
Arc::new(value)để tạo allocation chia sẻ, vàArc::clone(&arc)để tạo thêm owner mới (mỗi clone tăng count thêm 1 qua phép cộng atomic). - Biết
Arc<T>làSend + SynckhiT: Send + Sync— đây là lý do compiler cho phép truyền vàothread::spawnvớimoveclosure. - Hiểu cost của
Arc: atomic increment chậm hơn non-atomic ~10-100 lần về tỉ lệ, nhưng tuyệt đối vẫn rất nhanh (chỉ vài nano-giây) — không phải bottleneck thực tế. - Biết use case kinh điển: share read-only config / data lớn cho N worker thread mà không cần lock.
- Biết pattern
Arc<Mutex<T>>là cách phổ biến nhất để có shared mutable cross-thread — chi tiết ở Bài 233. - Dùng
Arc::downgrade(&arc)để lấyWeak<T>— tương tự cặpRc/Weaknhưng cho multi-thread, phá cycle trong graph có chu trình.
Arc<T> Là Gì
Arc<T> nằm trong std::sync, viết tắt của Atomically Reference Counted. Ý tưởng và API y hệt Rc<T> (Bài 215): mỗi allocation đi kèm strong + weak counter, ai cũng là owner ngang nhau, ai drop cuối thì lo dọn dẹp. Khác biệt duy nhất nhưng then chốt: các phép cộng/trừ counter là atomic.
Vì sao điều đó quan trọng? Khi hai thread cùng Rc::clone một lúc, hai phép cộng usize bình thường có thể chồng chéo trên CPU và counter bị sai. Sai count đồng nghĩa với double-free (giải phóng quá sớm — dangling reference) hoặc memory leak (không bao giờ về 0). Để chặn ở compile time, Rc được declare !Send và !Sync — không thể đưa cross-thread.
Arc giải bài toán đó bằng cách dùng AtomicUsize cho counter — phép cộng/trừ được CPU đảm bảo nguyên tử, hai thread cùng tăng cũng cho kết quả đúng. Vì vậy Arc<T> implement Send + Sync khi T: Send + Sync — bạn có thể yên tâm move nó vào closure cho thread::spawn.
Quy tắc thực dụng:
- Single-thread: dùng
Rc<T>— rẻ hơn, đủ dùng. - Multi-thread: dùng
Arc<T>— đắt hơn một chút nhưng an toàn (và compiler cũng sẽ ép bạn dùngArcnếu cầnSend).
Tên gọi nhấn mạnh "Atomic" cũng là lời nhắc: chỉ thay Rc bằng Arc khi thực sự cần — đừng phòng thân, overhead atomic luôn tồn tại.
Cú Pháp Arc::new
Khởi tạo một Arc bằng Arc::new:
use std::sync::Arc;
fn main() {
let arc = Arc::new(5);
println!("arc = {}", arc); // 5 — Arc deref tự động
println!("count = {}", Arc::strong_count(&arc)); // 1
}
Phân tích:
Arc::new(5)cấp một allocation trên heap chứa: value5+ strong count atomic (khởi tạo = 1) + weak count atomic (= 0). Trả vềArc<i32>.Arc<T>implementDereftrả về&T, nên dùng method/operator củaTtrực tiếp như khi cầm reference thường.Arc::strong_count(&arc)là associated function (không phải method), trả về số strong reference hiện tại — tiện cho debug.
Lưu ý cú pháp use std::sync::Arc; — Arc nằm trong module sync (gần với Mutex, RwLock), khác với Rc ở std::rc. Đây là lời nhắc rõ ràng: chỉ cần đụng tới Arc, bạn đang ở vùng đất concurrency.
Giống Rc: Arc::new nhận value by value (move). Bạn phải có ownership của T trước, không thể tạo Arc từ một reference có sẵn — bản chất allocation phải copy value vào heap kèm header counter.
Arc::clone Tăng Count Atomic
Tạo thêm owner bằng Arc::clone:
use std::sync::Arc;
fn main() {
let arc = Arc::new(String::from("hello"));
let a = Arc::clone(&arc);
let b = Arc::clone(&arc);
println!("count = {}", Arc::strong_count(&arc)); // 3
println!("{}, {}, {}", arc, a, b);
}
Mỗi Arc::clone(&arc) chỉ làm hai việc:
- Tăng strong count thêm 1 bằng atomic fetch_add với ordering
Relaxed— phép tăng nguyên tử ở mức CPU. - Trả về
Arcmới trỏ tới cùng allocation.
Vẫn là O(1), vẫn không deep clone String. Khác biệt duy nhất so với Rc::clone: phép cộng nguyên tử thay vì phép cộng thường — đắt hơn một chút (xem mục 6).
Convention vẫn là Arc::clone(&arc) chứ không phải arc.clone() — đọc rõ ý đồ "tăng reference count", tránh nhầm với deep clone của T. Đây là điều Rust Book và clippy đều khuyến nghị, và lại càng quan trọng trong context concurrency, nơi mỗi cost nhỏ đều có thể bị săm soi khi tuning.
// Cả hai cùng kết quả, nhưng:
let b = arc.clone(); // Mơ hồ: có thể là deep clone của T?
let c = Arc::clone(&arc); // Rõ ràng: chỉ tăng reference count atomic
Khi mỗi binding Arc ra khỏi scope, Drop trigger phép fetch_sub atomic giảm count đi 1. Owner cuối cùng (count chuyển từ 1 về 0) chạy Drop của T và giải phóng heap allocation — vẫn đúng "nguyên tắc cuối tắt đèn", không sớm không muộn.
Send Across Thread
Đây là điểm Arc tỏ rõ giá trị so với Rc: clone xong, move vào closure cho thread::spawn được ngay:
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3, 4, 5]);
let mut handles = vec![];
for i in 0..3 {
let data = Arc::clone(&data); // clone trước loop body
let h = thread::spawn(move || { // move clone vào thread
let sum: i32 = data.iter().sum();
println!("thread {i}: sum = {sum}");
});
handles.push(h);
}
for h in handles { h.join().unwrap(); }
println!("count cuối = {}", Arc::strong_count(&data)); // 1 — các thread đã drop
}
Quan sát:
- Trước mỗi
thread::spawn, taArc::clone(&data)rồimovebản clone đó vào closure — tăng count atomic 1 lần, mỗi thread giữ bảnArcriêng. thread::spawnyêu cầu closure phảiSend. Closure captureArc<Vec<i32>>; vìVec<i32>làSend + Sync,Arc<Vec<i32>>cũngSend + Sync→ compiler chấp nhận.- Khi mỗi thread kết thúc,
Arccapture trong closure drop → atomic decrement count.maingiữ 1Arccuối — đến cuối cùngVecmới được giải phóng.
Nếu thay Arc bằng Rc, compile sẽ fail với thông báo quen thuộc: "`Rc<Vec<i32>>` cannot be sent between threads safely — the trait `Send` is not implemented". Compiler không cho phép vi phạm safety, đúng tinh thần Bài 230.
Cost Arc vs Rc
Atomic operation phải qua memory barrier và đôi khi qua cache coherence protocol giữa các core, nên đắt hơn phép cộng thường. Con số tham khảo:
- Non-atomic increment (Rc): cỡ ~0.3 ns (1 cycle trên CPU 3 GHz), thường được CPU pipeline che hết.
- Atomic increment (Arc): cỡ ~5-30 ns trên x86_64 (chậm hơn ~10-100 lần về tỉ lệ).
Nghe "100x chậm hơn" có vẻ kinh hoàng nhưng cần giữ tỉ lệ: Arc::clone vẫn nằm trong vùng nano-giây. Nếu một request handler chỉ clone một số lần hữu hạn, tổng overhead vẫn dưới microsecond — không phải bottleneck. Bottleneck thực tế của Arc thường nằm ở chỗ khác: contention khi nhiều thread cùng tăng/giảm counter trên cùng allocation làm cache line liên tục bounce giữa các core.
Lời khuyên thực dụng:
- Đừng tối ưu sớm — nếu cần share cross-thread, dùng
Arc. - Nếu profile thấy
Arccontention cao, suy nghĩ: có cần share đến mức đó không? Có thể giảm số lượng clone, hoặc đổi sang pattern khác (channel, sharded data). - Trong single-thread, vẫn nên dùng
Rc— không có lý do trả overhead atomic vô ích.
Một con số ngữ cảnh: Arc::clone rẻ hơn vài bậc so với một lần Mutex::lock/unlock không tranh chấp (cỡ vài chục đến hàng trăm ns), và rẻ hơn nhiều bậc so với một context switch (cỡ vài microsecond).
Use Case: Share Config / Read-Only Data Multi-Thread
Use case kinh điển nhất của Arc: share read-only data cho N worker thread. Vì data không đổi, không cần lock — chỉ cần đảm bảo mọi thread cùng giữ một bản tham chiếu sống.
use std::sync::Arc;
use std::thread;
#[derive(Debug)]
struct Config {
api_url: String,
timeout_ms: u64,
max_retries: u32,
}
fn main() {
// Load config 1 lần, wrap vào Arc
let config = Arc::new(Config {
api_url: String::from("https://api.example.com"),
timeout_ms: 5000,
max_retries: 3,
});
// Spawn 4 worker, mỗi worker dùng config read-only
let mut handles = vec![];
for i in 0..4 {
let config = Arc::clone(&config);
let h = thread::spawn(move || {
// Tất cả các thread đọc cùng 1 Config trên heap
println!(
"worker {i}: call {} timeout={}ms retries={}",
config.api_url, config.timeout_ms, config.max_retries
);
});
handles.push(h);
}
for h in handles { h.join().unwrap(); }
}
Tại sao dùng Arc<Config> chứ không clone Config cho mỗi thread?
- Tiết kiệm bộ nhớ: nếu
Configchứa string lớn, danh sách rule, từ điển — clone 4 bản tốn 4x bộ nhớ.Arcshare đúng 1 bản, mỗi thread giữ con trỏ 8 byte. - Cache friendly: tất cả thread đọc cùng địa chỉ memory → CPU cache hit cao, không có invalidation vì không ai mutate.
- Code rõ ràng: muốn nói "đây là singleton shared, không thay đổi" thì
Arc<Config>diễn đạt trực tiếp.
Lưu ý: pattern này chỉ áp dụng khi data thực sự không đổi sau khi tạo Arc. Nếu cần update (reload config khi nhận signal), bạn cần Arc<RwLock<Config>> (Bài 234) hoặc arc-swap crate.
Arc<Mutex<T>> Cho Shared Mutable
Khi cần nhiều thread cùng mutate một data, chỉ Arc là chưa đủ — Arc<T> chỉ cho ra &T (immutable). Pattern phổ biến nhất là gói Mutex<T> bên trong Arc → Arc<Mutex<T>>: Arc lo share ownership cross-thread, Mutex lo exclusive write access.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0_u64));
let mut handles = vec![];
for _ in 0..8 {
let counter = Arc::clone(&counter);
let h = thread::spawn(move || {
for _ in 0..1000 {
let mut g = counter.lock().unwrap();
*g += 1; // mutate dưới lock
}
});
handles.push(h);
}
for h in handles { h.join().unwrap(); }
println!("counter = {}", *counter.lock().unwrap()); // 8000
}
Đọc nhanh: 8 thread, mỗi thread tăng counter 1000 lần dưới lock; kết quả cuối luôn là 8000, không bị race.
Pattern này phổ biến đến mức bạn sẽ gặp nó mỗi lần đụng concurrency Rust:
- Shared counter / stats — request count, error count, latency histogram.
- Shared queue / buffer — N producer push, M consumer pop.
- Shared cache — lookup miss thì lock, insert, unlock.
Cảnh báo nhỏ: Mutex không miễn phí (lock/unlock cỡ vài chục ns không tranh chấp, hàng trăm ns khi có contention), và lock càng to thì throughput càng thấp. Bài 232 sẽ đi sâu vào Mutex<T>, Bài 233 sẽ trình bày kỹ pattern Arc<Mutex<T>> cùng các lưu ý về poisoning, deadlock, và giữ lock càng ngắn càng tốt.
Weak Variant — Arc::downgrade
Cũng như Rc/rc::Weak, Arc có cặp đôi sync::Weak<T> — weak reference không sở hữu, không giữ data sống, chỉ cho biết "data đang còn hay đã chết". Dùng để phá cycle trong graph (parent ↔ child) khi cả hai đều là Arc.
use std::sync::{Arc, Weak};
fn main() {
let strong = Arc::new(String::from("data"));
let weak: Weak<String> = Arc::downgrade(&strong);
println!("strong = {}", Arc::strong_count(&strong)); // 1
println!("weak = {}", Arc::weak_count(&strong)); // 1
// Nâng cấp weak về Arc (chỉ thành công nếu data còn sống)
if let Some(arc) = weak.upgrade() {
println!("upgrade OK: {}", arc);
}
drop(strong);
// Sau khi strong cuối cùng drop, upgrade trả None
assert!(weak.upgrade().is_none());
}
Đặc điểm:
Arc::downgrade(&arc) -> Weak<T>tăng weak count atomic, không tăng strong count.Weak::upgrade(&self) -> Option<Arc<T>>trảSomenếu còn ít nhất 1 strong reference; trảNonenếu strong đã về 0 (data đã bị drop).- Weak không giữ data sống — đúng vai trò "tham chiếu yếu", không cản trở giải phóng.
Use case: parent Arc<Node> chứa Vec<Arc<Node>> children. Nếu child cũng muốn tham chiếu ngược lên parent, dùng Weak<Node> — tránh cycle parent ↔ child gây leak. Y hệt pattern Rc/Weak ở Bài 218, chỉ khác là an toàn cross-thread.
Tổng Kết
Arc<T>(Atomically Reference Counted, ởstd::sync) là phiên bản multi-thread củaRc<T>— counter dùng atomic instruction nên hai thread cùngclone/dropđều an toàn.Arc<T>làSend + SynckhiT: Send + Sync— đó là điều cho phép truyền quathread::spawnvớimoveclosure mà compiler chấp nhận.Arc::new(value)wrap value vào heap kèm counter atomic;Arc::clone(&arc)tăng strong count atomic 1, trả vềArcmới trỏ cùng allocation — O(1).- Cost: atomic increment ~5-30 ns trên x86_64, chậm hơn non-atomic ~10-100x về tỉ lệ nhưng vẫn nằm trong vùng nano-giây — không phải bottleneck thực tế, trừ khi có contention cực cao.
- Use case nổi bật: share read-only config / data lớn cho N worker thread — không cần lock vì không ai mutate; tiết kiệm bộ nhớ và cache friendly.
Arc<Mutex<T>>là pattern phổ biến nhất cho shared mutable cross-thread —Arclo ownership,Mutexlo exclusive write. Chi tiết ở Bài 233.Arc::downgrade(&arc) -> Weak<T>tạo weak reference không sở hữu — phá cycle trong graph multi-thread, dùngupgrade()để check còn sống.- Quy tắc chọn: single-thread + 1 owner →
Box; single-thread + n owner →Rc; multi-thread + n owner →Arc; cần mutate trong group cuối → thêmMutex/RwLock.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Vì sao
Rc<i32>không gửi được quathread::spawncònArc<i32>thì được? Compiler dựa vào marker trait nào để phân biệt? - Cho đoạn code
let a = Arc::new(0); let b = Arc::clone(&a); let c = Arc::clone(&b); drop(b); println!("{}", Arc::strong_count(&a));— output là bao nhiêu? - Bạn có một
Config200 KB cần chia sẻ cho 16 worker thread, hoàn toàn read-only. Có 2 phương án: (a) cloneConfigcho mỗi thread; (b) wrap vàoArc<Config>rồiArc::clone. Mỗi phương án tốn bao nhiêu bộ nhớ thêm? Nên chọn phương án nào? - Vì sao chỉ
Arc<T>chưa đủ khi nhiều thread cần mutate data? Pattern phổ biến nhất để giải quyết là gì? - Khi nào nên dùng
Weak<T>thay vìArc<T>trong môi trường multi-thread? Cho 1 ví dụ.
Đáp án
Rcdùng counter không atomic — nếu hai thread cùngclonesẽ race counter → unsafe. Để chặn ở compile time,Rcđược declare!Send + !Sync; closure chứaRckhôngSend→thread::spawnreject.Arcdùng counter atomic, an toàn → implementSend + Sync(khiT: Send + Sync) → compiler chấp nhận. Compiler dựa vào hai traitSendvàSync.- Output là
2. Saulet b = Arc::clone(&a)count = 2; saulet c = Arc::clone(&b)count = 3; saudrop(b)count = 2. Còn lạiavàcgiữ → strong count = 2. - (a) clone 16 bản tốn thêm 16 × 200 KB ≈ 3.2 MB cho data + chi phí allocation. (b)
Arc::new1 lần (1 × 200 KB + ~16 byte header counter),Arc::clone16 lần chỉ tốn 16 × 8 byte con trỏ trên stack = 128 byte — gần như miễn phí. Nên chọn (b) trong gần như mọi trường hợp read-only, vừa tiết kiệm bộ nhớ, vừa cache friendly, vừa diễn đạt rõ ý "share". - Vì
Arc<T>chỉ choDeref::Target = Ttrả về&T(immutable) — không cóDerefMut. Nhiều thread cùng có&mut Ttrên cùng data sẽ vi phạm borrow rule (data race). Pattern phổ biến nhất: gói trongMutex<T>→Arc<Mutex<T>>.Arclo share ownership cross-thread,Mutexlo exclusive write access; mỗi lần mutate phảilock().unwrap()để lấyMutexGuardrồi mới deref mut. - Khi cần một tham chiếu không sở hữu để tránh giữ data sống quá lâu hoặc phá cycle. Ví dụ: graph node có cấu trúc parent ↔ child, parent dùng
Vec<Arc<Node>>children (strong), child dùngWeak<Node>parent (weak) — tránh cycle (mọi cycle Arc-Arc đều leak vì count không bao giờ về 0).
Bài Tiếp Theo
Bài 232: Mutex<T> — Exclusive Lock — học cụ thể std::sync::Mutex: cú pháp Mutex::new(0), gọi lock().unwrap() trả về MutexGuard<T> (deref ra &mut T), guard drop tự động unlock, cơ chế poisoning khi thread giữ lock panic, và các pitfall thường gặp (giữ lock quá lâu, deadlock, lock ordering). Khi vững Mutex, Bài 233 ghép cả hai thành Arc<Mutex<T>> — pattern shared mutable phổ biến nhất.
