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

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());
}
Arc ใน Rust ย่อมาจากอะไร?
คุณ release Mutex lock ใน Rust ได้อย่างไร?
Mutex จะกลาย 'poisoned' เมื่อไหร่?
ทำไมต้องใช้ Arc<Mutex<T>> แทนที่จะใช้แค่ Mutex<T>?