本文 AI 產出,尚未審核

Rust 並行與多執行緒

主題:並行安全性(SendSync


簡介

在多核心時代,並行程式設計已成為提升效能的關鍵技術。Rust 之所以在系統程式領域廣受好評,最重要的原因之一,就是它在編譯期就能保證 資料競爭安全(data‑race safety)。這項保證的核心概念,就是兩個自動衍生的 marker trait:SendSync

  • Send 表示「可以安全地將所有權轉移到其他執行緒」;
  • Sync 表示「同一個值的參考可以同時被多個執行緒存取」。

了解這兩個 trait 為什麼存在、什麼情況下會自動實作、什麼時候需要手動實作,對於寫出正確且高效的並行程式至關重要。本文將以淺顯易懂的方式,從概念說明到實作範例,帶領讀者一步步掌握 Rust 的並行安全模型。


核心概念

1. Send:所有權跨執行緒

在 Rust 中,所有權(ownership) 是唯一的資源管理機制。Send 告訴編譯器:「把 T 的值從目前執行緒搬移到另一個執行緒是安全的」。大多數標準型別(如 i32StringVec<T>)都自動實作 Send

為什麼有 !Send

某些型別內部包含 非執行緒安全 的資源,例如 Rc<T>(引用計數指標)或 *mut T(裸指標)。這些型別若被搬移到其他執行緒,可能會導致同時存取同一記憶體而產生資料競爭。

範例 1:thread::spawn 需要 Send

use std::thread;

fn main() {
    let data = String::from("Hello, Rust!");
    // `String` implements Send, 所以可以搬到新執行緒
    let handle = thread::spawn(move || {
        println!("{}", data);
    });

    handle.join().unwrap();
}

重點move 關鍵字將 data 的所有權搬入閉包,編譯器檢查 String: Send,若不符合則編譯失敗。

範例 2:Rc<T> 不是 Send

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

fn main() {
    let rc = Rc::new(42);
    // 編譯錯誤: `Rc<i32>` does not implement `Send`
    // let handle = thread::spawn(move || {
    //     println!("{}", rc);
    // });
}

提示:若需要在多執行緒間共享資料,請改用 Arc<T>(原子引用計數),它實作了 SendSync

2. Sync:共享參考的安全性

Sync 的定義是「&T(不可變參考)在多執行緒中同時使用是安全的」。換句話說,如果 T: Sync,那麼 &T 可以被多個執行緒同時借用而不會產生競爭。

大部分的 不可變資料i32StringVec<T>)都自動實作 Sync。但如果型別內部使用了 內部可變性(例如 Cell<T>RefCell<T>),則不會自動實作 Sync

範例 3:Arc<T>Sync

use std::sync::Arc;
use std::thread;

fn main() {
    let numbers = Arc::new(vec![1, 2, 3, 4, 5]);

    let mut handles = Vec::new();
    for i in 0..5 {
        let nums = Arc::clone(&numbers);
        let handle = thread::spawn(move || {
            println!("thread {} sees {:?}", i, nums[i]);
        });
        handles.push(handle);
    }

    for h in handles {
        h.join().unwrap();
    }
}

Arc<T> 同時滿足 Send(可搬移)與 Sync(可共享),因此適合在多執行緒環境下共享 只讀 資料。

範例 4:RefCell<T> 不是 Sync

use std::cell::RefCell;
use std::sync::Arc;
use std::thread;

fn main() {
    let cell = Arc::new(RefCell::new(0));
    // 編譯錯誤: `RefCell<i32>` does not implement `Sync`
    // let handle = thread::spawn(move || {
    //     *cell.borrow_mut() += 1;
    // });
}

結論:若需要在多執行緒中修改資料,請改用 Mutex<T>RwLock<T> 或其他具備內部同步機制的型別。

3. 手動實作 Send / Sync(慎用)

在極少數情況下,我們會自行宣告型別為 SendSync,通常是因為型別包含 unsafe 原始指標或外部 C 函式庫的資源。千萬不要隨意 unsafe impl Send,除非能保證所有存取都是線程安全的。

範例 5:自訂裸指標型別手動實作 Send

use std::marker::PhantomData;
use std::thread;

// 假設我們有一個來自 C 的非同步 API
struct RawPtr<T> {
    ptr: *mut T,
    _marker: PhantomData<T>,
}

// 只要我們保證在不同執行緒不會同時寫入同一指標,就可以安全地實作 Send
unsafe impl<T> Send for RawPtr<T> {}

fn main() {
    let mut value = 10;
    let raw = RawPtr {
        ptr: &mut value as *mut i32,
        _marker: PhantomData,
    };

    let handle = thread::spawn(move || unsafe {
        // 直接解引用裸指標
        *raw.ptr = 20;
    });

    handle.join().unwrap();
    println!("value = {}", value); // 可能是 20,也可能是未定義行為
}

警告:上述程式碼在實務上極易產生未定義行為,僅作為「如何手動實作」的示範。若無法保證唯一性,請改用 Arc<Mutex<T>>


常見陷阱與最佳實踐

陷阱 說明 解決方式
誤以為 &mut T 自動 Send &mut T 本身不具備 Send,只有當 T: Send 時才會。 使用 Arc<Mutex<T>>Arc<RwLock<T>> 包裝可變資料。
thread::spawn 中直接使用 Rc Rc 不是 Send,會導致編譯錯誤。 改用 Arc,或在單執行緒內部使用 Rc
忘記 Sync 需求的共享參考 只要有 &T 被多執行緒同時借用,就需要 T: Sync 使用 Mutex<T> 包裝需要內部可變性的資料,或改寫為無內部可變性。
手動 unsafe impl Send/Sync 若沒有徹底檢查,極易產生資料競爭或未定義行為。 盡量避免;若必須,務必在文件中說明安全前提,並寫單元測試驗證。
static 變數中使用非 Sync 型別 static 變數在多執行緒間是全域共享的,必須是 Sync 使用 lazy_static!once_cell::sync::Lazy 搭配 Mutex/RwLock 包裝。

最佳實踐小結

  1. 預設使用安全抽象ArcMutexRwLockchannel 等都是經過驗證的同步原語。
  2. 最小化共享範圍:盡量將資料所有權保持在單一執行緒,僅在必要時才共享。
  3. 利用編譯器檢查:讓編譯器幫你捕捉 Send / Sync 的違規,除非真的需要 unsafe,否則不要自行實作。
  4. 寫測試:對於自行實作的 Send/Sync,加入多執行緒測試,確保不會產生競爭。

實際應用場景

1. Web 伺服器的請求處理

在使用 tokioasync-std 等非阻塞執行環境時,常見的做法是把 共享的設定或緩存 放入 Arc<RwLock<T>>,讓每個工作執行緒(或 task)可以同時讀取,必要時再寫入。

use std::sync::{Arc, RwLock};
use tokio::task;

#[derive(Debug, Clone)]
struct Config {
    max_connections: usize,
}

#[tokio::main]
async fn main() {
    let cfg = Arc::new(RwLock::new(Config { max_connections: 100 }));

    // 模擬 10 個同時處理的請求
    let mut handles = Vec::new();
    for i in 0..10 {
        let cfg_clone = Arc::clone(&cfg);
        handles.push(task::spawn(async move {
            // 只讀設定
            let read = cfg_clone.read().unwrap();
            println!("task {} sees max = {}", i, read.max_connections);
        }));
    }

    for h in handles {
        h.await.unwrap();
    }
}

2. 資料庫連線池

連線池本質上是一個 可共享且可變 的容器。典型實作會把池子包在 Arc<Mutex<Vec<Connection>>>,每次取得連線時鎖住池子,使用完畢再釋放。

3. 多媒體編碼/解碼

編碼器往往需要 大量 CPU,會把工作分配給多個執行緒。每個執行緒只處理自己的緩衝區(Vec<u8>),而共享的設定則放在 Arc<Config>,確保 Config: Send + Sync


總結

  • SendSync 是 Rust 保證 資料競爭安全 的兩大基石。
  • Send所有權 能安全搬移至其他執行緒;Sync不可變參考 能在多執行緒中同時使用。
  • 大多數標準型別自動實作這兩個 trait,只有涉及 內部可變性裸指標 的型別會被排除。
  • 避免自行 unsafe impl Send/Sync,除非能提供嚴格的安全證明;否則使用 ArcMutexRwLock 等安全抽象。
  • 在實務開發中,將共享資源包裝成 Arc<...>,配合適當的同步原語,既能保持高效能,又能讓編譯器幫你檢查錯誤。

掌握了 SendSync 的概念後,你就能在 Rust 中自信地寫出 安全、可擴充且效能卓越 的多執行緒程式。祝你在並行程式設計的旅程中玩得開心! 🚀