iT邦幫忙

2025 iThome 鐵人賽

DAY 27
0
Rust

把前端加速到天花板:Rust+WASM 即插即用外掛系列 第 27

Day 27|Send / Sync、Arc/Mutex 與那些不能跨線的型別

  • 分享至 

  • xImage
  •  

如果你來自 C++ 世界,你對多執行緒的印象大概是:

“能編譯就能跑,只是結果可能不對。”

Rust 的哲學剛好相反:

“能跑就表示結果一定對——因為不對的根本不能編譯。”

Rust 的「跨執行緒安全」靠兩個核心 trait 保證:

  • Send:可以安全地被移轉(moved)到另一個 thread;
  • Sync:可以安全地被多個 thread 同時借用(shared reference)。

這兩個 trait 幾乎是 Rust 並行模型的憲法。

Send 與 Sync 是什麼?

  • Send:如果型別 T 是 Send,表示它的擁有權可以安全移交到另一條執行緒。
  • Sync:如果型別 T 是 Sync,表示你可以同時在多執行緒間「共享 &T(不可變借用)」。

用公式表示:

T: Send      → 可跨執行緒 move
&T: Sync     → 可跨執行緒共享引用

例如:

use std::thread;

let v = vec![1, 2, 3];
thread::spawn(move || {
    println!("{:?}", v);
}).join().unwrap(); // OK

Vec<T> 是 Send 的,所以可以被 move 進另一條 thread。

但以下這段會 編譯失敗

use std::rc::Rc;
use std::thread;

let r = Rc::new(42);
thread::spawn(move || {
    println!("{r}");
});

錯誤訊息:

Rc<i32> cannot be sent between threads safely

因為 Rc<T> 不是 Send(它的引用計數不是 atomic),如果多執行緒同時操作,會造成 race condition。Rust 不允許這樣的型別跨線。

什麼型別是 Send / Sync?

你不需要手動實作,大部分標準型別都自動實作了這兩個 trait。

類型 Send Sync 說明
i32, f64, bool, String, Vec<T> 值語意,可安全複製或共享
Rc<T> 非 thread-safe RC
Arc<T> 原子計數,可跨線
Mutex<T> 鎖住內部 T,保證排他存取
RefCell<T> 僅限單執行緒 runtime 借用檢查
Cell<T> 同上
*mut T / *const T 原始指標,不具安全語義

Rust 編譯器會自動根據型別內含內容決定能不能跨線。
這一點與 C++ 相反——在 C++,所有東西理論上都能跨線;在 Rust,只有被認證安全的才行。


Arc 與 Mutex

要共享資料怎麼辦?在 Rust,你不能用 Rc,要用:

  • Arc(Atomic Reference Counted):跨線共享擁有權;
  • Mutex(Mutual Exclusion):跨線共享可變資料。

一起用時的典型寫法:

use std::sync::{Arc, Mutex};
use std::thread;

let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];

for _ in 0..5 {
    let counter = Arc::clone(&counter);
    let handle = thread::spawn(move || {
        let mut num = counter.lock().unwrap();
        *num += 1;
    });
    handles.push(handle);
}

for h in handles { h.join().unwrap(); }
println!("result = {}", *counter.lock().unwrap());

拆解:

  1. Arc::new(Mutex::new(0))
    → 多執行緒共享同一個整數。
  2. Arc::clone
    → 每條 thread 都拿一份計數器的共享引用。
  3. .lock()
    → Mutex 取得鎖,確保一次只有一個 thread 修改。

這種組合 (Arc<Mutex<T>>) 幾乎是所有跨線共享狀態的起點。
而且它自動實作 SendSync,所以不需要 unsafe

那些「不能跨線」的型別

Rust 的 borrow checker 會擋掉任何可能造成資料競爭的型別:

型別 為什麼不能跨線?
Rc<T> 非原子計數,多線 race
RefCell<T> 借用檢查在 runtime,不 thread-safe
Cell<T> 同上,只適合單線修改
*mut T / *const T 原始指標,不知道誰擁有
&mut T 可變引用只能唯一存在,跨線會破壞規則

這些型別在單執行緒下非常好用(例如 GUI event loop 或 wasm),
但只要放進 thread::spawn,編譯器就會馬上報紅。
Rust 把「data race」變成編譯錯誤,而不是 runtime 驚喜。


你真的需要多執行緒嗎?

這是 Rust 想讓你思考的問題。
在 C++,我們容易先開 thread 再想同步;
Rust 則鼓勵你先想「擁有權的分工」。

一個常見的 pattern 是:

  • 如果資料只在內部 mutate,用 Mutex<T>
  • 如果只需要併發讀取,用 Arc<T>
  • 如果需要任務分派,用 channel 傳遞訊息(std::sync::mpsc 或 tokio channel)。

Rust 的並行模型更接近「資料流分工」而不是「共享狀態爭奪」。
你不需要鎖全域變數,只需要把資料送給下一個擁有者。

C++ vs. Rust

面向 C++ Rust
執行緒建立 任意傳參 move 進閉包需滿足 Send
任何 mutex 都能鎖任何物件 Send + 'static 才能跨線
RC 計數 shared_ptr 不分執行緒 Rc 限單線,Arc 限跨線
借用檢查 runtime/手動紀律 編譯期強制
錯誤型態 race → UB/crash race → compile error

上一篇
Day 26|TransferableStructured CloneSharedArrayBuffer
下一篇
Day 28|Rust 學習資源與工具
系列文
把前端加速到天花板:Rust+WASM 即插即用外掛30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言