Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu vì sao
Arc<Mutex<T>>là pattern mặc định để chia sẻ mutable state giữa nhiều thread trong Rust. - Phân biệt vai trò hai lớp:
Arc= shared ownership an toàn cross-thread,Mutex= interior mutability có khoá độc quyền. - Viết được pattern clone Arc trước rồi move bản clone vào
thread::spawn— không phải clone giá trị bên trong. - Viết counter demo: N thread mỗi thread tăng
1000lần, saujoinđọc đúngN*1000. - Nắm nguyên tắc lock granularity: giữ critical section ngắn, không I/O trong khi giữ lock, dùng
drop()để release sớm. - Hiểu contention: nhiều thread cùng tranh một
Mutexsẽ tạo wait queue — biết khi nào cân nhắc atomic lock-free cho counter đơn giản. - Biết
Arc<RwLock>là biến thể cho workload read-heavy (preview B234) và khác biệt cốt lõi so vớiArc<Mutex>. - Tránh được common mistake: quên
Arc::clonetrong loop spawn dẫn đến lỗi "value moved".
Bài học xây dựng trên Bài 232: Mutex<T> — Exclusive Lock (cơ chế một lock riêng lẻ) và Bài 231: Arc<T> — Atomic Reference Count (chia sẻ ownership read-only). Combo của hai mảnh đó tạo nên pattern hôm nay.
Pattern Cấu Trúc Arc + Mutex
Vấn đề: bạn có một giá trị (số đếm, HashMap cache, Vec log...) và muốn nhiều thread cùng đọc lẫn ghi. Rust một lúc không cho phép:
Arc<T>một mình: chia sẻ được giữa thread nhưng chỉ cho immutable reference — không sửa được.Mutex<T>một mình: cho interior mutability có khoá, nhưngMutex<T>không tự clone được — không thể move một bản vào mỗi thread.Rc<RefCell<T>>: pattern tương đương single-thread đã học ở B208 —Rckhông phảiSend, không qua thread được.RefCellcũng khôngSync.
Giải pháp: ghép hai mảnh thread-safe tương ứng — Arc thay Rc (atomic count, Send + Sync), Mutex thay RefCell (khoá thật thay vì runtime borrow tracker). Kết quả là Arc<Mutex<T>>:
- Lớp ngoài
Arc: clone để mỗi thread giữ một handle, tăng/giảm atomic reference count khi clone/drop. Khi count về 0,Mutex<T>bên trong được drop. - Lớp trong
Mutex: tại runtime, mỗi thread khi muốn đụng vàoTphải gọi.lock()— block đến khi giành được khoá, trả vềMutexGuardhoạt động như&mut T. Drop guard tự unlock.
Hình dung: Arc là "vé vào nhà", Mutex là "chiếc chìa khoá phòng" — ai cũng có vé vào nhà (clone Arc), nhưng cùng lúc chỉ một người cầm được chìa (lock thành công).
Cú Pháp Khởi Tạo
Khởi tạo "từ trong ra ngoài": tạo Mutex bọc giá trị trước, rồi bọc Arc bên ngoài. Hai dòng quen thuộc:
use std::sync::{Arc, Mutex};
fn main() {
let shared: Arc<Mutex<i32>> = Arc::new(Mutex::new(0));
// Đọc / ghi từ main thread
{
let mut g = shared.lock().unwrap();
*g += 10;
} // g drop → Mutex unlock
println!("value = {}", *shared.lock().unwrap()); // 10
}
Quan sát:
- Type đầy đủ
Arc<Mutex<i32>>— annotate cho dễ đọc, thường bỏ qua được nhờ inference. .lock()trảLockResult<MutexGuard<T>>—unwrap()để bỏ qua trường hợp poison (đã giải thích ở B232).- Guard
gderef tới&mut T— viết*g += 10. - Đặt block
{ ... }hoặc để guard rơi tự nhiên cuối scope để unlock. Đọc lần sau trongprintln!tạo lock tạm rồi unlock ngay.
Với giá trị phức tạp hơn: Arc::new(Mutex::new(HashMap::new())), Arc::new(Mutex::new(Vec::new())), Arc::new(Mutex::new(MyState { ... })) — pattern y hệt.
Clone Arc, Share Vào Thread
Để mỗi thread giữ riêng một handle, ta clone Arc trước khi thread::spawn(move || ...). Clone Arc chỉ tăng atomic reference count, không clone Mutex bên trong:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let shared = Arc::new(Mutex::new(0));
let c = Arc::clone(&shared);
let h = thread::spawn(move || {
*c.lock().unwrap() += 1;
});
h.join().unwrap();
println!("after thread: {}", *shared.lock().unwrap()); // 1
}
Lưu ý vài điểm thường nhầm:
- Clone trước, move sau.
let c = Arc::clone(&shared);tạo handle mới, sau đómove || ...capturecchứ không captureshared.sharedở lại main để main đọc kết quả. - Dùng
Arc::clone(&shared)thay vìshared.clone(). Hai cách cùng kết quả, nhưng cú pháp đầu nêu rõ "đây là clone Arc, không phải cloneT" — giúp đọc code phân biệt với clone giá trị thật (đắt). movebắt buộc. Closure phải sở hữucđể chuyển qua thread khác — borrow không an toàn vì main có thể return trước thread.
Counter Multi-Thread Demo
Bài tập "Hello world" của Arc<Mutex>: N thread, mỗi thread tăng counter 1000 lần, kỳ vọng tổng cuối là N * 1000. Nếu thiếu khoá, kết quả thường nhỏ hơn do race condition; với Mutex kết quả luôn đúng:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
const N: usize = 8;
const ITER: usize = 1000;
let counter = Arc::new(Mutex::new(0u64));
let mut handles = Vec::with_capacity(N);
for _ in 0..N {
let c = Arc::clone(&counter);
handles.push(thread::spawn(move || {
for _ in 0..ITER {
let mut g = c.lock().unwrap();
*g += 1;
}
}));
}
for h in handles {
h.join().unwrap();
}
let total = *counter.lock().unwrap();
println!("total = {} (expect {})", total, (N * ITER) as u64); // 8000
}
Vài điểm cần nhớ khi đọc code:
- Mỗi thread giữ một
Arcriêng (clone bên ngoài loop, không clone trong loop của thread). - Mỗi vòng lặp bên trong lock → tăng → drop guard. Critical section cực ngắn (một phép cộng).
- Sau khi tất cả thread
join, main đọccounter— vẫn dùng.lock()dù lúc này chắc chắn không có ai tranh. - Kết quả deterministic
N*ITER = 8000. BỏMutexđi (dùngArc<UnsafeCell>) là race condition và kết quả thấp hơn — bài học cốt lõi của bài này.
Lock Granularity
Nguyên tắc vàng: giữ lock càng ngắn càng tốt. Mỗi mili-giây bạn cầm khoá là một mili-giây các thread khác phải chờ. Hai sai lầm thường gặp: gọi I/O trong critical section, và "ôm" giá trị guard chạy logic phức tạp không cần khoá.
use std::sync::{Arc, Mutex};
use std::thread;
fn process(_x: i32) -> String { /* tính toán nặng, không động đến shared */ String::new() }
fn bad(counter: &Arc<Mutex<i32>>) {
let mut g = counter.lock().unwrap();
*g += 1;
// BAD: I/O trong khi giữ lock — block mọi thread khác
println!("counter is {}", *g);
let _ = process(*g);
} // g drop ở đây — lock giữ rất lâu
fn good(counter: &Arc<Mutex<i32>>) {
let snapshot = {
let mut g = counter.lock().unwrap();
*g += 1;
*g
}; // drop g sớm — unlock ngay
println!("counter is {}", snapshot);
let _ = process(snapshot);
}
fn good_explicit(counter: &Arc<Mutex<i32>>) {
let mut g = counter.lock().unwrap();
*g += 1;
let snapshot = *g;
drop(g); // release sớm, không đợi cuối scope
println!("counter is {}", snapshot);
let _ = process(snapshot);
}
fn main() {
let c = Arc::new(Mutex::new(0));
let h = thread::spawn({ let c = Arc::clone(&c); move || good(&c) });
h.join().unwrap();
}
Ba quy tắc thực hành:
- Không I/O trong critical section:
println!, đọc file, gọi network — tất cả phải xảy ra sau khi unlock. - Snapshot value rồi unlock: copy giá trị cần ra biến local, drop guard, tính toán bên ngoài.
- Dùng
drop(g)explicit khi scope tự nhiên còn dài — code rõ ý đồ hơn là dựa scope.
Contention Performance
Contention = nhiều thread cùng cố lock một Mutex tại cùng thời điểm. OS đặt các thread thua vào wait queue, đánh thức tuần tự khi lock free. Nhiều thread + critical section dài + hot path tăng cao = throughput sụp đổ — đôi khi chạy single-thread còn nhanh hơn vì đỡ overhead context switch.
Với counter đơn giản, có một giải pháp lock-free dùng std::sync::atomic:
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::thread;
fn main() {
let counter = Arc::new(AtomicU64::new(0));
let mut handles = Vec::new();
for _ in 0..8 {
let c = Arc::clone(&counter);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
c.fetch_add(1, Ordering::Relaxed);
}
}));
}
for h in handles { h.join().unwrap(); }
println!("{}", counter.load(Ordering::Relaxed)); // 8000
}
So sánh nhanh:
Arc<Mutex<T>>: linh hoạt, dùng cho mọi kiểu dữ liệu (collection, struct phức tạp), nhưng có overhead lock + risk contention.Arc<AtomicU64>: nhanh hơn nhiều lần cho integer counter, không lock — nhưng chỉ áp dụng được cho integer/bool/pointer (sẽ học chi tiết ở B235-B236).
Quy tắc đơn giản: counter / flag dùng atomic; structure dùng Mutex; read-heavy structure dùng RwLock (mục sau).
Pattern vs Arc<RwLock>
Arc<Mutex<T>> luôn exclusive: cùng lúc đúng một thread giữ lock, bất kể đọc hay ghi. Với workload read-heavy (1 ghi : 100 đọc, ví dụ config snapshot, route table) điều này phí phạm: các reader hoàn toàn có thể đọc song song mà không xung đột.
Arc<RwLock<T>> là biến thể cho phép nhiều reader đồng thời hoặc một writer duy nhất:
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let cfg = Arc::new(RwLock::new(String::from("v1")));
// Nhiều reader song song
let mut readers = Vec::new();
for i in 0..4 {
let c = Arc::clone(&cfg);
readers.push(thread::spawn(move || {
let g = c.read().unwrap();
println!("reader {}: {}", i, *g);
}));
}
// Writer chiếm exclusive
{
let mut g = cfg.write().unwrap();
*g = String::from("v2");
}
for r in readers { r.join().unwrap(); }
}
Khác biệt cốt lõi:
Mutex::lock()→MutexGuardluôn exclusive.RwLock::read()→RwLockReadGuard, nhiều reader giữ cùng lúc.RwLock::write()→RwLockWriteGuard, exclusive nhưMutex.
Chi tiết về starvation, writer preference và benchmark sẽ ở Bài 234: RwLock<T>. Tạm thời nhớ: balanced read/write → Mutex; read >> write → cân nhắc RwLock.
Common Mistake
Sai lầm phổ biến nhất: quên clone Arc trong loop spawn. Người mới hay viết:
// SAI — không compile
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let shared = Arc::new(Mutex::new(0));
for _ in 0..4 {
thread::spawn(move || {
*shared.lock().unwrap() += 1; // move shared lần đầu OK, lần thứ hai compile error
});
}
}
Compiler báo "use of moved value: `shared`" — vòng lặp thứ hai không còn shared để move. Fix đúng: clone Arc trước khi đưa vào closure, mỗi vòng lặp một clone riêng:
// OK
let shared = Arc::new(Mutex::new(0));
for _ in 0..4 {
let c = Arc::clone(&shared); // clone trước
thread::spawn(move || {
*c.lock().unwrap() += 1; // closure sở hữu c
});
}
// shared (handle gốc) vẫn còn ở scope này, sau loop có thể dùng tiếp
Vài sai lầm khác liên quan:
- Clone giá trị thay vì clone Arc:
let c = shared.lock().unwrap().clone();rồi movec— như vậy mỗi thread giữ bản copy độc lập, không chia sẻ. Nếu mục tiêu là share, phải cloneArc. - Lạm dụng
staticglobal: tạostatic COUNTER: Mutex<i32> = Mutex::new(0);rồi truy cập khắp nơi — dễ viết nhưng khó test, khó thay thế, khó scope. PatternArc<Mutex>truyền tham số rõ ràng vẫn nên là mặc định; chỉ dùng global khi thật sự cần và đã cân nhắc. - Lock hai lần trong cùng một thread: gọi
shared.lock()rồi trong khi guard còn sống lại gọishared.lock()lần nữa → deadlock self.std::sync::Mutexkhông re-entrant.
Tổng Kết
Arc<Mutex<T>>là pattern mặc định chia sẻ mutable state giữa nhiều thread:Arc= shared ownership atomic,Mutex= interior mutability có khoá.- Cú pháp khởi tạo từ trong ra ngoài:
let shared = Arc::new(Mutex::new(value));. - Để chia sẻ giữa thread:
let c = Arc::clone(&shared);rồithread::spawn(move || { *c.lock().unwrap() += 1; });— clone Arc, không clone giá trị bên trong. - Counter demo N thread tăng
ITERlần luôn cho đúngN*ITER— không race condition vìMutexbảo vệ. - Lock granularity: critical section càng ngắn càng tốt; tránh I/O trong khi giữ lock; snapshot value rồi unlock; dùng
drop(g)để release sớm. - Contention: nhiều thread cùng lock → wait queue, throughput giảm. Counter đơn giản nên dùng
AtomicU64::fetch_addlock-free. Arc<Mutex>luôn exclusive; workload read-heavy nên cân nhắcArc<RwLock>để nhiều reader song song (B234).- Mistake hay gặp: quên clone Arc trong loop spawn; clone giá trị thay vì clone Arc; lạm dụng global static; lock đôi self-deadlock.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Vì sao không thể chỉ dùng
Mutex<T>mà không cóArckhi chia sẻ giữa nhiều thread? Và ngược lại, vì saoArc<T>đơn thuần không đủ? - Viết counter 4 thread, mỗi thread tăng
500lần, in tổng cuối — kỳ vọng2000. - Khác biệt cú pháp giữa
Arc::clone(&shared)vàshared.clone()? Tại sao convention prefer cách đầu? - Trong critical section, bạn cần đọc value, gọi
http_post(value)rồi log kết quả. Refactor để chỉ giữ lock khi đọc value, không giữ trong khi HTTP request. - Workload nào nên dùng
Arc<Mutex>, nào nênArc<RwLock>, nào nênArc<AtomicU64>? Cho 3 ví dụ. - Đoạn code
for _ in 0..4 { thread::spawn(move || { *shared.lock().unwrap() += 1; }); }báo lỗi gì khi compile? Sửa lại đúng.
Đáp án
Mutex<T>không tự clone được — không thể move "một bản" vào mỗi thread; thread đầu tiên move xong, thread thứ hai không có gì để giữ.Arc<T>chia sẻ được nhưng chỉ cho immutable reference; không sửa được nội dung. Cần cả hai:Arcđể mỗi thread có handle riêng,Mutexđể cho phép interior mutability có khoá.let c = Arc::new(Mutex::new(0u64)); let mut h = Vec::new(); for _ in 0..4 { let cc = Arc::clone(&c); h.push(thread::spawn(move || { for _ in 0..500 { *cc.lock().unwrap() += 1; } })); } for x in h { x.join().unwrap(); } println!("{}", *c.lock().unwrap());→2000.- Cùng kết quả (cùng tăng atomic ref count). Convention
Arc::clone(&shared)nêu rõ "đây là clone Arc rẻ", còn.clone()trong codebase lớn dễ bị nhầm là clone giá trị thật (đắt). Đọc code rõ ý đồ hơn. let v = { let g = shared.lock().unwrap(); *g }; let resp = http_post(v); log::info!("{:?}", resp);. Lock chỉ giữ trong block đọc, sau đó unlock; HTTP request và log chạy ngoài lock — các thread khác không bị block.Arc<Mutex>: HashMap cache có cả read/write thường xuyên, Vec log mỗi thread append.Arc<RwLock>: config snapshot 1000 reader / 1 writer/giờ, route table nhiều handler đọc / hot-reload đôi khi ghi.Arc<AtomicU64>: counter request, flag shutdown, counter byte-processed.- Lỗi "use of moved value: `shared`" — closure
movetrong vòng lặp đầu đã moveshared, vòng sau không còn. Sửa: thêmlet c = Arc::clone(&shared);trong loop, closure dùngcthay vìshared.
Bài Tiếp Theo
Bài 234: RwLock<T> — Many Reader, Single Writer — tiếp tục pattern shared state nhưng với khoá đọc/ghi tách biệt: read() cho nhiều reader đồng thời, write() exclusive một writer. Học cú pháp, khi nào hơn Mutex, vấn đề starvation khi writer phải chờ liên tục, và benchmark thực tế với workload read-heavy.
