ข้ามไปยังเนื้อหา

Arc และ Mutex

ใน JavaScript Workers ไม่แชร์ memory โดยปริยาย แต่ละ Worker มี heap ของตัวเอง วิธีเดียวที่จะแชร์ mutable state คือผ่าน SharedArrayBuffer ร่วมกับ Atomics ที่เป็น low-level มีโอกาสเกิด error ง่าย และแทบไม่ถูกใช้ใน application code

Rust รองรับ shared mutable state ข้าม threads ผ่านสอง type ที่ใช้ร่วมกัน:

  • Arc<T> — Atomically Reference Counted เป็น thread-safe smart pointer ที่ให้หลาย thread own ค่าเดียวกันบน heap เมื่อ Arc clone สุดท้ายถูก drop ค่าจะถูก free
  • Mutex<T> — Mutual exclusion lock ครอบค่า T และรับประกันว่ามีแค่ thread เดียวที่เข้าถึง inner value ได้ในแต่ละขณะ

รวมกัน Arc<Mutex<T>> คือ idiom มาตรฐานของ Rust สำหรับ shared mutable state ข้าม threads

Arc คือคำตอบของ Rust ต่อโจทย์ “อยากมี owner หลายรายข้าม thread” หน้าตาคล้าย Rc<T> (reference counter แบบ single-thread) แต่ใช้ atomic operation กับ reference count จึงอัปเดตจากหลาย thread พร้อมกันได้อย่างปลอดภัย

คุณ clone Arc เพื่อให้ thread อื่นมี handle ไปยัง allocation เดียวกัน การ clone มีต้นทุนต่ำมาก: แค่ increment atomic integer

Mutex<T> ครอบค่าและป้องกันด้วย lock ในการเข้าถึง T ภายใน คุณเรียก .lock() ซึ่ง:

  1. Block จนกว่าไม่มี thread อื่นถือ lock
  2. คืนค่า MutexGuard<T> — smart pointer ที่ dereference เป็น &mut T
  3. Release lock อัตโนมัติเมื่อ MutexGuard ถูก drop (ปลาย scope)

นี่คือ RAII locking — pattern เดียวกับที่ Rust ใช้กับ memory: resource จะถูก release อัตโนมัติเมื่อ guard ออกจาก scope

TypeScript
// TypeScript: ไม่มี shared mutable state ระหว่าง Workers โดยปริยาย
// SharedArrayBuffer + Atomics คือ primitive ระดับต่ำ
const sab = new SharedArrayBuffer(4);
const arr = new Int32Array(sab);
// Worker 1
Atomics.add(arr, 0, 1); // atomic increment
// Worker 2
Atomics.add(arr, 0, 1); // atomic increment
// Main thread
console.log(Atomics.load(arr, 0)); // 2
// ในทางปฏิบัติ TS apps ส่วนใหญ่หลีกเลี่ยง shared memory ระหว่าง Workers
// และใช้ message passing (postMessage) แทน
Rust
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// Arc<Mutex<u32>>: shared, mutable counter ข้าม threads
let counter = Arc::new(Mutex::new(0u32));
let mut handles = vec![];
for _ in 0..5 {
let c = Arc::clone(&counter); // ถูก: แค่ increment atomic refcount
let h = thread::spawn(move || {
let mut val = c.lock().unwrap(); // acquire lock
*val += 1; // mutate inner value
// MutexGuard 'val' ถูก drop ที่นี่ — lock ถูก release
});
handles.push(h);
}
for h in handles { h.join().unwrap(); }
// มีแค่ owner เดียวเหลืออยู่ — ปลอดภัยที่จะอ่านค่าสุดท้าย
println!("final counter = {}", *counter.lock().unwrap()); // 5
}

Pattern ที่พบบ่อยคือให้แต่ละ thread push ผลลัพธ์เข้าไปใน Vec ที่แชร์กัน แล้วอ่านผลรวมหลังจาก thread ทั้งหมด join แล้ว

TypeScript
// TypeScript: รวบรวมผลลัพธ์ด้วย Promise.all
async function processAll(items: number[]): Promise<number[]> {
return Promise.all(items.map(item =>
Promise.resolve(item * item) // simulate async work
));
}
Rust
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let results: Arc<Mutex<Vec<u32>>> = Arc::new(Mutex::new(vec![]));
let items = vec![1u32, 2, 3, 4, 5];
let mut handles = vec![];
for item in items {
let r = Arc::clone(&results);
let h = thread::spawn(move || {
r.lock().unwrap().push(item * item);
});
handles.push(h);
}
for h in handles { h.join().unwrap(); }
let mut res = results.lock().unwrap().clone();
res.sort(); // sort เพื่อ output ที่แน่นอน
println!("squares: {:?}", res); // squares: [1, 4, 9, 16, 25]
}
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0u32));
let mut handles = vec![];
for _ in 0..5 {
let c = Arc::clone(&counter);
let h = thread::spawn(move || {
let mut val = c.lock().unwrap();
*val += 1;
});
handles.push(h);
}
for h in handles {
h.join().unwrap();
}
println!("final counter = {}", *counter.lock().unwrap());
}
struct Guard;
impl Guard {
fn value(&self) -> Option<i32> {
None // pretend the shared state was empty
}
}
impl Drop for Guard {
fn drop(&mut self) {
println!(" lock released");
}
}
struct Lock;
impl Lock {
fn read(&self) -> Guard {
println!(" lock acquired");
Guard
}
}
fn main() {
let lock = Lock;
// `lock.read()` is a temporary inside the condition.
if let Some(v) = lock.read().value() {
println!(" got {v}");
} else {
// In edition 2024 the guard is already released here,
// so taking a write lock in this branch would NOT deadlock.
println!(" else branch: safe to take the write lock");
}
println!("after the if let");
}
Arc ใน Rust ย่อมาจากอะไร?
คุณ release Mutex lock ใน Rust ได้อย่างไร?
Mutex จะกลาย 'poisoned' เมื่อไหร่?
ทำไมต้องใช้ Arc<Mutex<T>> แทนที่จะใช้แค่ Mutex<T>?