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

Threads

standard library ของ Rust มี OS thread ผ่าน std::thread::spawn คุณส่ง closure และฟังก์ชันจะส่งคืน JoinHandle<T> — handle ที่คุณใช้รอให้ thread เสร็จสิ้นและดึงค่าที่ส่งคืน

use std::thread;
fn main() {
let handle = thread::spawn(|| {
// This runs on a new OS thread
42
});
let result = handle.join().unwrap(); // block until the thread finishes
println!("Thread returned: {}", result);
}

closure จะเป็น move || { ... } เมื่อต้องการ capture ตัวแปรจาก scope ที่ครอบอยู่ — ไม่เช่นนั้นคอมไพเลอร์ไม่สามารถรับประกันได้ว่าข้อมูลที่ถูก capture จะมีอายุยาวพอ

เนื่องจาก thread อาจมีอายุยาวกว่า scope ที่ถูก spawn ข้อมูลใดก็ตามที่ thread capture จะต้องเป็นเจ้าของโดย thread คำสั่ง move ย้าย ownership ของตัวแปรที่ถูก capture เข้าสู่ closure

use std::thread;
fn main() {
let message = String::from("hello from the main thread");
let handle = thread::spawn(move || {
// message is moved here — the thread now owns it
println!("{}", message);
});
handle.join().unwrap();
// println!("{}", message); // would not compile — moved above
}

ถ้าตัด move ออก closure จะพยายาม ยืม message แทนที่จะเป็นเจ้าของ คอมไพเลอร์ปฏิเสธทันที เพราะพิสูจน์ไม่ได้ว่า borrow นี้จะยังใช้ได้ตลอดเวลาที่ thread ที่ spawn อาจรันอยู่

// @expect-error E0373
use std::thread;
fn main() {
let message = String::from("hello");
let handle = thread::spawn(|| {
println!("{}", message);
});
handle.join().unwrap();
}
error[E0373]: closure may outlive the current function, but it borrows `message`, which is owned by the current function
--> src/main.rs:5:32
|
5 | let handle = thread::spawn(|| {
| ^^ may outlive borrowed value `message`
6 | println!("{}", message);
| ------- `message` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src/main.rs:5:18
|
5 | let handle = thread::spawn(|| {
| __________________^
6 | | println!("{}", message);
7 | | });
| |______^
help: to force the closure to take ownership of `message` (and any other referenced variables), use the `move` keyword
|
5 | let handle = thread::spawn(move || {
| ++++

thread::spawn บังคับให้ต้อง 'static เพราะไม่มีอะไรหยุด thread ที่ spawn ไม่ให้มีอายุยาวกว่าฟังก์ชันที่สร้างมัน thread::scope ให้การรับประกันที่แข็งแรงกว่าแทน — ทุก thread ที่ spawn ภายใน scope จะถูก join อัตโนมัติก่อนที่ scope จะ return ดังนั้นการ borrow ข้อมูล local ที่ไม่ใช่ 'static จึงปลอดภัย — ไม่ต้อง Arc ไม่ต้อง clone ไม่ต้อง move ownership เลย

use std::thread;
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
thread::scope(|s| {
s.spawn(|| {
let sum: i32 = numbers.iter().sum();
println!("sum: {sum}");
});
s.spawn(|| {
let max = numbers.iter().max().unwrap();
println!("max: {max}");
});
}); // both threads are joined here, automatically
println!("numbers is still usable: {:?}", numbers);
}
sum: 15
max: 5
numbers is still usable: [1, 2, 3, 4, 5]

ใช้ thread::scope ทุกครั้งที่ thread ต้องมีอายุแค่เท่ากับหนึ่งการเรียกฟังก์ชัน — มันใช้พิธีกรรมน้อยกว่า Arc<Mutex<T>> อย่างชัดเจน และให้การรับประกันของ borrow checker แบบเดียวกับที่คุณเชื่อถือในโค้ด single-threaded อยู่แล้ว

เพื่อแชร์ข้อมูลที่ mutable ระหว่างหลาย thread ให้ห่อข้อมูลด้วย Arc<Mutex<T>>:

  • Mutex<T> ให้ mutual exclusion — มีแค่ thread เดียวที่สามารถล็อคได้ในแต่ละครั้ง
  • Arc<T> (Atomically Reference Counted) ให้หลาย thread ถือ pointer ที่นับ reference ไปยัง allocation เดียวกัน

คุณ clone Arc ก่อนย้าย clone นั้นเข้าไปในแต่ละ thread Mutex ที่อยู่ข้างในถูกแชร์ ไม่ใช่ clone

use std::thread;
use std::sync::{Arc, Mutex};
fn main() {
let counter = Arc::new(Mutex::new(0_i32));
let mut handles = Vec::new();
for _ in 0..4 {
let counter = Arc::clone(&counter); // cheap ref-count increment
let h = thread::spawn(move || {
let mut n = counter.lock().unwrap(); // acquire lock
*n += 1;
}); // lock released when n drops
handles.push(h);
}
for h in handles {
h.join().unwrap();
}
println!("Final count: {}", *counter.lock().unwrap());
}

lock().unwrap() ไม่ใช่แค่ shortcut ขี้เกียจ — lock() คืนค่าเป็น Result และมันจะเป็น Err ในสถานการณ์เดียวเท่านั้น: อีก thread หนึ่ง panic ขณะถือ lock เดียวกันอยู่ เมื่อเกิดเหตุการณ์นั้น Mutex จะถูกทำเครื่องหมายว่า poisoned เพราะ thread ที่ panic อาจทิ้งข้อมูลที่ป้องกันไว้ในสถานะที่อัปเดตครึ่งเดียว ไม่สอดคล้องกัน

use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let data = Arc::new(Mutex::new(vec![1, 2, 3]));
let d = Arc::clone(&data);
let handle = thread::spawn(move || {
let mut guard = d.lock().unwrap();
guard.push(4);
panic!("worker thread panics while holding the lock");
});
let _ = handle.join(); // Err — the panic already printed to stderr
match data.lock() {
Ok(_) => println!("lock was not poisoned"),
Err(poisoned) => {
// The data is still there — into_inner() recovers it despite poisoning.
let guard = poisoned.into_inner();
println!("recovered data despite poisoning: {:?}", guard);
}
}
}
thread '<unnamed>' (<id>) panicked at src/main.rs:11:9:
worker thread panics while holding the lock
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
recovered data despite poisoning: [1, 2, 3, 4]

.lock() ครั้งต่อ ๆ ไปทุกครั้งบน Mutex ตัวนี้จะคืนค่า Err ตลอดไป — poisoning ไม่หายไปเอง PoisonError::into_inner() ให้คุณกู้คืน guard ได้อยู่ดี (ข้อมูลไม่ได้เสียหายในกรณีนี้ push ทำงานเสร็จก่อน panic!) ซึ่งเป็นการตัดสินใจที่ถูกต้องเมื่อคุณรู้ว่า invariant ยังคงอยู่ การเรียก .unwrap() กับทุก lock() แบบไม่คิด — เหมือนที่เกือบทุกตัวอย่างในบทนี้ทำเพื่อความกระชับ — หมายความว่า worker thread ที่ panic เพียงตัวเดียวจะทำให้การเรียก .lock().unwrap() ในอนาคตทุกที่ในโปรแกรม panic ไปด้วย

lock guard เป็นค่า temporary และ edition 2024 เปลี่ยนจังหวะการ drop temporary ที่อยู่ในเงื่อนไขของ if let บน Rust 2021 read guard ข้างล่างจะยังมีชีวิตอยู่ตลอด branch else การขอ write lock ตรงนั้นจึง deadlock — thread รอ lock ที่ตัวเองถืออยู่ ส่วนบน Rust 2024 guard จะถูก drop ก่อนเข้า else:

use std::sync::RwLock;
fn ensure_initialized(value: &RwLock<Option<bool>>) {
if let Some(x) = *value.read().unwrap() {
println!("already set to {x}");
} else {
// Edition 2024 drops the read guard before this branch runs,
// so taking the write lock here cannot deadlock.
let mut v = value.write().unwrap();
if v.is_none() {
*v = Some(true);
println!("initialized");
}
}
}
fn main() {
let cell = RwLock::new(None);
ensure_initialized(&cell);
ensure_initialized(&cell);
}

ผลลัพธ์คือ initialized แล้วตามด้วย already set to true ถ้า compile ด้วย edition = "2021" โปรแกรมเดียวกันจะค้างตั้งแต่การเรียกครั้งแรก ถ้าอยากได้ scope แบบเดิมกลับมา ให้เขียนเงื่อนไขเป็น match แทน — match ยังถือ temporary ไว้ตลอดทั้ง expression

Mutex<i32> เกินความจำเป็นสำหรับอะไรง่าย ๆ อย่าง counter ที่แชร์กัน — ทุกการเพิ่มค่าต้องจ่ายค่า lock/unlock เต็มรอบ ประเภทใน std::sync::atomic ทำการเพิ่มค่าเป็นคำสั่ง CPU เดียวที่แบ่งแยกไม่ได้ ไม่มี lock ไม่มี Result และไม่ต้องคิดเรื่อง poisoning เลย:

use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::Arc;
use std::thread;
fn main() {
let counter = Arc::new(AtomicI32::new(0));
let mut handles = Vec::new();
for _ in 0..4 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
counter.fetch_add(1, Ordering::SeqCst);
}
}));
}
for h in handles {
h.join().unwrap();
}
println!("Final count: {}", counter.load(Ordering::SeqCst));
}
Final count: 4000

ผลลัพธ์เดียวกับเวอร์ชัน Arc<Mutex<i32>> ข้างบน โดยไม่มี lock เลยสักตัว Ordering::SeqCst (sequentially consistent) เป็น ordering ที่เข้าใจง่ายที่สุดและเป็นค่าเริ่มต้นที่ปลอดภัย — ordering ที่อ่อนกว่า (Relaxed, Acquire/Release) มีไว้สำหรับตอนที่ profiling แสดงว่า synchronization ส่วนเกินนั้นมีต้นทุนจริง ๆ ใช้ atomic เมื่อ shared state เป็นตัวเลขหรือ flag เดียวจริง ๆ — ทันทีที่คุณต้องอัปเดตค่าที่เกี่ยวข้องกัน สอง ค่าพร้อมกัน คุณต้องการ Mutex (หรือ RwLock) เพื่อทำให้การอัปเดตนั้นเป็น atomic ทั้งก้อน

รูปแบบที่พบบ่อยคือให้แต่ละ thread คำนวณค่าและ push เข้าไปใน Vec ที่แชร์กัน หลังจาก thread ทั้งหมดเสร็จสิ้น เรา sort ผลลัพธ์เพื่อให้ output เป็น deterministic ไม่ว่า scheduling order จะเป็นอย่างไร

use std::thread;
use std::sync::{Arc, Mutex};
fn main() {
let results = Arc::new(Mutex::new(Vec::new()));
let mut handles = Vec::new();
for i in 0..5 {
let results = Arc::clone(&results);
let handle = thread::spawn(move || {
let square = i * i;
let mut v = results.lock().unwrap();
v.push(square);
});
handles.push(handle);
}
for h in handles {
h.join().unwrap();
}
let mut final_results = results.lock().unwrap().clone();
final_results.sort();
println!("Squares: {:?}", final_results);
let sum: i32 = final_results.iter().sum();
println!("Sum: {}", sum);
}

Arc<Mutex<T>> ทำงานข้าม thread ได้เพราะตัวนับ reference ของ Arc เป็น atomic ส่วนตัวนับของ Rc<T> เป็นแค่ integer ธรรมดาที่ไม่ atomic — ถ้าสอง thread เพิ่มค่าพร้อมกันก็จะเกิด race คอมไพเลอร์จับสิ่งนี้ได้ตั้งแต่ระดับ type ก่อนโค้ดจะรันด้วยซ้ำ:

// @expect-error E0277
use std::rc::Rc;
use std::thread;
fn main() {
let data = Rc::new(42);
let handle = thread::spawn(move || {
println!("{}", data);
});
handle.join().unwrap();
}
error[E0277]: `Rc<i32>` cannot be sent between threads safely
--> src/main.rs:6:32
|
6 | let handle = thread::spawn(move || {
| ------------- ^------
| | |
| __________________|_____________within this `{closure@src/main.rs:6:32: 6:39}`
| | |
| | required by a bound introduced by this call
7 | | println!("{}", data);
8 | | });
| |_____^ `Rc<i32>` cannot be sent between threads safely
|
= help: within `{closure@src/main.rs:6:32: 6:39}`, the trait `Send` is not implemented for `Rc<i32>`

เปลี่ยน Rc::new เป็น Arc::new ก็แก้ปัญหาได้ทันที — Arc<i32> เป็น Send closure จึง capture ประเภทที่ปลอดภัยที่จะย้ายข้าม thread boundary พูดสั้น ๆ ในหนึ่งประโยค: Rc<T> เป็น !Send เพราะตัวนับ reference ของมันคือการเพิ่ม/ลดค่า Cell<usize> ธรรมดาที่ไม่มี synchronization เลย — ถ้าสอง thread แข่งกันแก้ค่านั้นก็จะทำให้ตัวนับเองเสียหาย คอมไพเลอร์จึงห้ามตรง ๆ แทนที่จะเสี่ยงกับ race นั้น

thread::scope ตัดข้อกำหนด 'static ออกไป แต่ไม่ได้ตัดกฎ exclusivity ของ borrow checker ออก — สอง scoped thread ไม่สามารถถือ mutable borrow ของตัวแปรเดียวกันพร้อมกันได้ ด้วยเหตุผลเดียวกับที่สอง &mut reference อยู่ร่วมกันไม่ได้ในโค้ด single-threaded

// @expect-error E0499
use std::thread;
fn main() {
let mut counter = 0;
thread::scope(|s| {
s.spawn(|| {
counter += 1; // first mutable borrow
});
s.spawn(|| {
counter += 1; // ERROR: second mutable borrow of counter at the same time
});
});
println!("{counter}");
}
error[E0499]: cannot borrow `counter` as mutable more than once at a time
--> src/main.rs:11:17
|
7 | thread::scope(|s| {
| - has type `&'1 Scope<'1, '_>`
8 | s.spawn(|| {
| - -- first mutable borrow occurs here
| _________|
| |
9 | | counter += 1; // first mutable borrow
| | ------- first borrow occurs due to use of `counter` in closure
10 | | });
| |__________- argument requires that `counter` is borrowed for `'1`
11 | s.spawn(|| {
| ^^ second mutable borrow occurs here
12 | counter += 1; // ERROR: second mutable borrow of counter at the same time
| ------- second borrow occurs due to use of `counter` in closure
|
note: requirement that the value outlives `'1` introduced here
--> library/std/src/thread/scoped.rs:203:35
|
203 | F: FnOnce() -> T + Send + 'scope,
| ^^^^^^
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0499`.

นี่คือแก่นของ “fearless concurrency” ในหนึ่ง diagnostic: บั๊กที่จะเป็น data race แบบเงียบ ๆ (สอง thread เพิ่มค่า counter พร้อมกันโดยไม่มี synchronization ทำให้อัปเดตหายไป) ใน C หรือ Go จะไม่มีทางได้ binary ที่รันได้เลยใน Rust แก้แบบเดียวกับที่คุณแก้ &mut สองตัวที่ขัดแย้งกันในโค้ด single-threaded — synchronize ด้วย Mutex/AtomicI32 หรือจัดโครงสร้างใหม่ให้แต่ละ thread เป็นเจ้าของข้อมูลคนละส่วนกัน

borrow checker จับได้เฉพาะความขัดแย้งที่ type system มองเห็น — มันมองไม่เห็นว่า เมื่อไหร่ ที่ runtime สอง thread จะรอ lock ที่อีกฝั่งถืออยู่ การล็อค mutex สองตัวในลำดับที่ไม่สอดคล้องกันระหว่าง thread คือ deadlock คลาสสิก:

sequenceDiagram
  participant T1 as Thread 1
  participant A as lock_a
  participant B as lock_b
  participant T2 as Thread 2
  T1->>A: lock "A"
  T2->>B: lock "B"
  T1->>B: wants "B" — blocked, T2 holds it
  T2->>A: wants "A" — blocked, T1 holds it
  Note over T1,T2: Neither thread can proceed — deadlock
สอง thread ล็อคคนละลำดับทำให้ deadlock
// @no-run
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
fn main() {
let lock_a = Arc::new(Mutex::new(0));
let lock_b = Arc::new(Mutex::new(0));
let (a1, b1) = (Arc::clone(&lock_a), Arc::clone(&lock_b));
let t1 = thread::spawn(move || {
let _g1 = a1.lock().unwrap();
thread::sleep(Duration::from_millis(50)); // give t2 time to take lock_b
let _g2 = b1.lock().unwrap(); // waits for t2 to release lock_b — forever
println!("t1 acquired both locks");
});
let (a2, b2) = (Arc::clone(&lock_a), Arc::clone(&lock_b));
let t2 = thread::spawn(move || {
let _g1 = b2.lock().unwrap();
thread::sleep(Duration::from_millis(50)); // give t1 time to take lock_a
let _g2 = a2.lock().unwrap(); // waits for t1 to release lock_a — deadlock
println!("t2 acquired both locks");
});
t1.join().unwrap();
t2.join().unwrap();
// This program compiles cleanly and then hangs forever — neither thread
// ever prints, and the process never exits.
}

t1 ล็อค lock_a ก่อนแล้วอยาก lock_b; t2 ล็อค lock_b ก่อนแล้วอยาก lock_a แต่ละ thread sleep นานพอที่จะรับประกันว่าอีกฝั่งได้ล็อคตัวแรกไปแล้ว ทำให้ทั้งสอง thread ลงเอยด้วยการรอ lock ที่อีกฝั่งถืออยู่ — ตลอดไป ทางแก้คือวินัยที่คอมไพเลอร์บังคับให้คุณไม่ได้: ล็อคหลาย lock ตามลำดับ global เดียวกันเสมอทั่วทั้งโปรแกรม (เช่น ล็อค lock_a ก่อน lock_b เสมอ) ด้วยลำดับที่สอดคล้องกัน thread หนึ่งจะชนะการแข่งขันเพื่อ lock แรกเสมอ และ thread ที่สองก็แค่รอคิวของตัวเองแทนที่จะ deadlock

std::thread::available_parallelism() รายงานว่ามี thread กี่ตัวที่รันพร้อมกันได้จริงบนเครื่องปัจจุบัน (โดยประมาณคือจำนวน CPU core):

use std::thread::available_parallelism;
fn main() {
let n = available_parallelism().map(|p| p.get()).unwrap_or(1);
println!("available parallelism is at least 1: {}", n >= 1);
}
available parallelism is at least 1: true

การ spawn OS thread มากกว่าที่ available_parallelism() รายงานไว้มาก ๆ ไม่ได้เพิ่ม throughput สำหรับงานที่ผูกกับ CPU — มันเพิ่มแค่ overhead ของการสลับ context ใช้มัน (หรือ crate thread-pool ที่ทำแบบนี้ให้แล้ว) เพื่อกำหนดขนาดของ worker pool แทนการเดาตัวเลขคงที่

ทุก synchronization primitive ในบทนี้จริง ๆ แล้วตอบคำถามเดียวกับที่ borrow checker ถามอยู่แล้วในโค้ด single-threaded — “ใครมีสิทธิ์แตะข้อมูลนี้ตอนนี้” — เพียงแค่บังคับใช้ข้าม thread แทนที่จะอยู่ในฟังก์ชันเดียว Arc ตอบว่า “ใครเป็นเจ้าของ” (ทุกคน ผ่าน shared ownership ที่นับ reference) Mutex/RwLock ตอบว่า “ใครแตะได้ตอนนี้” (ใครก็ตามที่ถือ guard — exclusivity แบบเดียวกับ &mut T แต่เช็คตอน runtime แทน compile time) Send/Sync ตอบว่า “ส่งสิ่งนี้ไปให้ thread อื่นได้เลยตามกฎหมายไหม” — เช็คตอน compile time ก่อนที่กลไก runtime ใด ๆ จะทำงาน

thread::scope ชนะ Arc<Mutex<T>> ทุกครั้งที่ thread มีอายุเท่ากับหนึ่งการเรียกฟังก์ชัน — โค้ดน้อยกว่า ไม่ต้อง heap allocation สำหรับ Arc ไม่ต้อง lock Arc<Mutex<T>> คือสิ่งที่คุณใช้เมื่อ thread (หรือข้อมูลเอง) ต้องมีอายุยาวกว่าฟังก์ชันที่ spawn มัน atomic ชนะ Mutex สำหรับค่า primitive เดี่ยว — ไม่มี lock ไม่มี poisoning — แต่หยุดเป็นตัวเลือกทันทีที่สองค่าที่เกี่ยวข้องต้องเปลี่ยนพร้อมกัน RwLock ชนะ Mutex เฉพาะเมื่อการอ่านมีจำนวนมากกว่าการเขียนอย่างมาก ในงานที่อ่าน/เขียนผสมกันหรือเขียนหนัก Mutex ธรรมดาง่ายกว่าและมักเร็วพอ ๆ กัน เพราะการติดตาม reader ของ RwLock มี overhead ของตัวมันเอง

  • “Arc<Mutex<T>> คือวิธีมาตรฐานในการแชร์ state ระหว่าง thread” มันเป็นค่าเริ่มต้นเมื่อ thread ต้องมีอายุยาวกว่าฟังก์ชันที่ spawn เมื่อไม่ใช่ thread::scope จะเลี่ยง Arc และ lock ไปได้ทั้งหมด เมื่อ shared state เป็นตัวเลขหรือ flag เดียว atomic ก็เลี่ยง lock ได้เช่นกัน
  • “Mutex ที่ poisoned หมายความว่าข้อมูลข้างในเสียหาย” poisoning หมายความแค่ว่า มี thread หนึ่ง panic ขณะถือ lock — มันไม่ได้บอกอะไรเลยว่าข้อมูลจริง ๆ อยู่ในสถานะเสียหายหรือไม่ into_inner() ให้คุณตรวจสอบแล้วตัดสินใจเอง ส่วนการ .unwrap() ทุก lock() ปฏิบัติกับ “อาจเสียหาย” เหมือนกับ “เสียหายแน่นอน” แล้ว panic ไม่ว่ากรณีไหน
  • “ถ้า compile ผ่าน ก็ deadlock ไม่ได้” borrow checker ป้องกัน data race (การเข้าถึงพร้อมกันโดยไม่ synchronize) ไม่ใช่ deadlock (สอง thread รอ lock ของกันและกันตลอดไป) โปรแกรมที่ deadlock เป็น Rust ที่ถูกต้องสมบูรณ์และไม่มี race เลย — มันแค่ไม่คืบหน้าไปไหน

โค้ด Rust ที่ AI เขียนมักเลือก Arc<Mutex<i32>> สำหรับ counter ที่แชร์กัน แม้ว่าการอัปเดตจะเป็นแค่การเพิ่มค่าง่าย ๆ ครั้งเดียว — เพราะเป็น pattern ที่มันเจอบ่อยที่สุดใน tutorial — โดยไม่คิดว่า atomic จะทำงานแบบเดียวกันได้เป๊ะ ๆ ด้วย overhead น้อยกว่าและไม่ต้องจัดการ Result/poisoning

🐛 หา bug ในโค้ดนี้

use std::sync::{Arc, Mutex};
use std::thread;
fn count_events(n: usize) -> i32 {
let counter = Arc::new(Mutex::new(0));
let mut handles = Vec::new();
for _ in 0..n {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut guard = counter.lock().unwrap();
*guard += 1;
}));
}
for h in handles {
h.join().unwrap();
}
*counter.lock().unwrap()
}
เฉลย

โค้ดนี้ทำงานได้ แต่ทุกการเพิ่มค่าต้องจ่ายค่าการ acquire/release lock เต็มรอบ และ .lock().unwrap() จะทำให้ทั้งโปรแกรม panic ทันทีที่ผู้ใช้ Mutex นี้คนอื่นเกิด panic ขณะถือ lock เนื่องจาก shared state เป็นแค่ i32 เดี่ยวที่ถูกเพิ่มค่า — ไม่ใช่สองค่าที่ต้องเปลี่ยนพร้อมกัน — AtomicI32 กับ fetch_add(1, Ordering::SeqCst) ทำงานแบบเดียวกันเป๊ะโดยไม่มี lock ไม่มี Result ให้ .unwrap() และไม่ต้องคิดเรื่อง poisoning เลย ใช้ Mutex/RwLock ก็ต่อเมื่อคุณต้องอัปเดตค่าที่เกี่ยวข้องกันมากกว่าหนึ่งค่าเป็นขั้นตอน atomic เดียว

JoinHandle::join() ทำอะไร?
ทำไม thread closure จึงมักใช้คำสั่ง move?
บทบาทของ Arc ใน Arc<Mutex<T>> คืออะไร?
thread หนึ่ง panic ขณะถือ lock ของ Mutex เกิดอะไรขึ้นกับ Mutex นั้นหลังจากนั้น?
โปรแกรมนี้ทำอะไร?
use std::thread;
fn main() {
    let mut counter = 0;
    thread::scope(|s| {
        s.spawn(|| counter += 1);
        s.spawn(|| counter += 1);
    });
    println!("{counter}");
}
ทำไมโค้ดนี้ถึง deadlock แทนที่จะทำงานเสร็จ?
// t1: locks a, sleeps, then locks b
// t2: locks b, sleeps, then locks a