Send 與 Sync 標記特徵(marker traits)
在多數語言裡,一個值能否安全地跨執行緒使用,是你記在腦中與註解裡的事實,弄錯了就得到一個資料競爭,只在負載下、在正式環境裡、有時才現身。Rust 用兩個標記特徵 Send 與 Sync 把那個事實拉進型別系統。它們沒有方法——它們只是編譯器貼到型別上的標籤,用來記錄「這個型別可安全地移動到另一條執行緒」(Send)或「這個型別可安全地在執行緒間共享」(Sync)。有了它們,編譯器就能拒絕編譯那些會把不對的東西送過執行緒邊界的程式碼。
精確地說:若把一個型別之值的所有權轉移到另一條執行緒(把它「移動」過去)是安全的,這個型別就是 Send。若多條執行緒同時持有對同一個值的共享參考(&T)是安全的,這個型別就是 Sync——等價地說,T 是 Sync 正好就在 &T 是 Send 的時候。多數型別自動同時是兩者。這些是「自動特徵」:若你結構的每個欄位都是 Send/Sync,編譯器就免費替你的結構實作它們,依結構往上傳遞這個性質,所以你幾乎從不自己寫 Send 或 Sync。少數型別則刻意不是。Rc<T>(單執行緒的參考計數)既非 Send 也非 Sync,因為它的計數在沒有同步下更新,對它競爭會損毀它——所以編譯器就直接不讓一個 Rc 跨越執行緒邊界,而你改採 Arc。原始指標也不是 Send/Sync,這正是為什麼包住未同步的共享狀態並試圖共享它會編不過。生成執行緒的 API 要求它們的閉包(以及它捕捉的一切)是 Send,那就是這一切被強制執行的關卡。
為何重要:Send 與 Sync 正是「無畏並行」實際運作的方式——安全 Rust 裡資料競爭的不存在,不是對良好行為的承諾,而是這兩個特徵結合別名與可變互斥規則後的一個編譯期後果。誠實的提醒:標記特徵專管資料競爭,不管死結或邏輯競爭,所以 Send/Sync 並不阻止你寫出一個會死結的程式。又因為它們是從型別內容推導出的自動特徵,單一個非 Send 的欄位(像深埋其中的一個 Rc)就讓整個結構變成非 Send——這是對的,但在你追出是哪個欄位作怪之前,可能是個令人困惑的編譯錯誤。在 unsafe 程式碼裡你可以用 unsafe impl Send 親手實作它們,那時就是「你」在承諾編譯器無法驗證的那份執行緒安全。
use std::rc::Rc; use std::sync::Arc; use std::thread; let shared = Arc::new(42); // Arc 「是」Send + Sync let a = Arc::clone(&shared); thread::spawn(move || println!("{a}")).join().unwrap(); // OK let local = Rc::new(42); // Rc 「既非」Send 也非 Sync // thread::spawn(move || println!("{local}")); // error[E0277]:`Rc<i32>` 無法在執行緒間安全傳送
Send/Sync 由型別的欄位自動推導;一個非 Send 欄位(像 Rc)就讓整個型別變成非 Send,而 spawn API 會執行它。
Send/Sync 防止資料競爭,而非死結或邏輯競爭。它們是自動推導的,所以單一個非執行緒安全的欄位就讓整個型別失格。在 unsafe 程式碼裡你可以寫 unsafe impl Send,但那時就是你親自為編譯器無法檢查的安全背書。