Danh sách bài viết

Bài 233: Arc<Mutex<T>> Pattern — Shared Mutable Multi-Thread

Bài 233 của series Rust Cơ Bản — khi cần chia sẻ một giá trị có thể thay đổi giữa nhiều thread, Rust không cho bạn dùng &mut T trực tiếp; borrow checker không cho hai thread cùng giữ mutable reference. Pattern chính tắc giải bài toán này là Arc<Mutex<T>>: Arc cấp shared ownership an toàn giữa nhiều thread (atomic reference count), Mutex bọc giá trị trong cơ chế interior mutability có khoá để mỗi thời điểm chỉ một thread đụng vào nội dung. Combo này phổ biến đến mức gần như mọi codebase Rust multi-thread đều có nó — counter, cache, registry, statistics. Bài này dạy: cú pháp khởi tạo Arc::new(Mutex::new(0)), kỹ thuật clone Arc trước rồi move vào thread::spawn, demo counter N thread tăng 1000 lần kỳ vọng tổng N*1000, nguyên tắc lock granularity giữ critical section càng ngắn càng tốt (tránh I/O trong khi giữ lock), hệ quả contention khi nhiều thread cùng tranh lock, so sánh với Arc<RwLock> cho workload read-heavy (preview B234), và lỗi phổ biến quên clone Arc trong loop spawn dẫn đến compile error.

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

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 1000 lần, sau join đọc đúng N*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 Mutex sẽ 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ới Arc<Mutex>.
  • Tránh được common mistake: quên Arc::clone trong 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.

2

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ưng Mutex<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 — Rc không phải Send, không qua thread được. RefCell cũng không Sync.

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ào T phải gọi .lock() — block đến khi giành được khoá, trả về MutexGuard hoạ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).

3

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 g deref 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 trong println! 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.

4

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 || ... capture c chứ không capture shared. 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 clone T" — giúp đọc code phân biệt với clone giá trị thật (đắt).
  • move bắt buộc. Closure phải sở hữu c để chuyển qua thread khác — borrow không an toàn vì main có thể return trước thread.
5

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 Arc riê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 đọc counter — 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ùng Arc<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.
6

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.
7

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).

8

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()MutexGuard luô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/writeMutex; read >> write → cân nhắc RwLock.

9

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 move c — 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 clone Arc.
  • Lạm dụng static global: tạo static 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. Pattern Arc<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ọi shared.lock() lần nữa → deadlock self. std::sync::Mutex không re-entrant.
10

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ồi thread::spawn(move || { *c.lock().unwrap() += 1; }); — clone Arc, không clone giá trị bên trong.
  • Counter demo N thread tăng ITER lần luôn cho đúng N*ITER — không race condition vì Mutex bả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_add lock-free.
  • Arc<Mutex> luôn exclusive; workload read-heavy nên cân nhắc Arc<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.
11

Bài Tập Củng Cố

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

  1. Vì sao không thể chỉ dùng Mutex<T> mà không có Arc khi chia sẻ giữa nhiều thread? Và ngược lại, vì sao Arc<T> đơn thuần không đủ?
  2. Viết counter 4 thread, mỗi thread tăng 500 lần, in tổng cuối — kỳ vọng 2000.
  3. Khác biệt cú pháp giữa Arc::clone(&shared)shared.clone()? Tại sao convention prefer cách đầu?
  4. 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.
  5. Workload nào nên dùng Arc<Mutex>, nào nên Arc<RwLock>, nào nên Arc<AtomicU64>? Cho 3 ví dụ.
  6. Đ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
  1. 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á.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. Lỗi "use of moved value: `shared`" — closure move trong vòng lặp đầu đã move shared, vòng sau không còn. Sửa: thêm let c = Arc::clone(&shared); trong loop, closure dùng c thay vì shared.
12

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.