本文 AI 產出,尚未審核

Rust 並行與多執行緒:使用 spawn 建立執行緒

簡介

在現代硬體上,CPU 核心數量持續增加,單一執行緒已無法充分發揮效能。並行程式設計成為提升效能、改善使用者體驗的關鍵。Rust 以「安全」為核心,提供了零成本抽象的執行緒模型,使開發者能在不犧牲安全性的前提下,輕鬆利用多核心。

std::thread::spawn 是 Rust 建立新執行緒的最直接方式。透過它,我們可以把一段閉包 (closure) 交給作業系統排程,與主執行緒同步或非同步執行。本文將從概念、實作、常見陷阱到實務應用,完整說明如何在 Rust 中使用 spawn


核心概念

1. spawn 的基本語法

use std::thread;

fn main() {
    // 建立一個新執行緒,執行閉包內的程式碼
    let handle = thread::spawn(|| {
        println!("我是子執行緒!");
    });

    // 主執行緒繼續執行
    println!("我是主執行緒!");

    // 等待子執行緒結束
    handle.join().unwrap();
}
  • thread::spawn 會回傳 JoinHandle<T>T 為閉包的回傳型別。
  • join() 會阻塞當前執行緒,直到子執行緒結束,並傳回子執行緒的結果或 panic 訊息。

2. 傳遞資料給子執行緒

Rust 的所有權規則仍然適用。若要將資料搬移 (move) 給子執行緒,必須使用 move 關鍵字:

use std::thread;

fn main() {
    let data = vec![1, 2, 3, 4];

    let handle = thread::spawn(move || {
        // data 已被搬移到此閉包內
        println!("子執行緒取得資料: {:?}", data);
    });

    // 這裡已無法再使用 data
    handle.join().unwrap();
}
  • 搬移 (move) 會將資料的所有權從主執行緒轉移到子執行緒,避免同時存取產生競爭條件。
  • 若需要共享資料,請改用 同步原語(如 ArcMutex)。

3. 使用 Arc + Mutex 共享可變資料

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

fn main() {
    // 使用 Arc 讓多個執行緒共享所有權,Mutex 保障可變存取的安全
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..5 {
        let c = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            // 取得鎖定,修改資料
            let mut num = c.lock().unwrap();
            *num += 1;
            println!("執行緒內部計數: {}", *num);
        });
        handles.push(handle);
    }

    // 等待所有子執行緒結束
    for h in handles {
        h.join().unwrap();
    }

    println!("最終計數: {}", *counter.lock().unwrap());
}
  • Arc<T>(Atomic Reference Counted)允許跨執行緒共享所有權。
  • Mutex<T> 提供互斥鎖,確保同時間只有一個執行緒能修改資料。
  • 這樣的組合是 Rust 中最常見的共享可變狀態 實作方式。

4. 取得子執行緒的回傳值

spawn 允許閉包回傳任意型別,只要在 join() 時正確處理即可:

use std::thread;

fn main() {
    let handle = thread::spawn(|| -> usize {
        // 計算 1~100 的總和
        (1..=100).sum()
    });

    // join() 會回傳 Result<T, Box<dyn Any + Send + 'static>>
    let sum = handle.join().expect("執行緒發生 panic");
    println!("1~100 的總和是 {}", sum);
}
  • 若子執行緒 panic,join() 會回傳 Err,必須使用 expectunwrap 或自行處理錯誤。

5. 限制執行緒數量:使用 ThreadPool

直接呼叫 spawn 會為每個任務建立新執行緒,過多執行緒會耗盡系統資源。實務上常使用 執行緒池(ThreadPool):

use std::sync::mpsc;
use std::thread;

fn main() {
    // 建立一個簡易的執行緒池,固定 4 個執行緒
    let (tx, rx) = mpsc::channel();
    let mut workers = vec![];

    for id in 0..4 {
        let rx = rx.clone();
        let worker = thread::spawn(move || {
            while let Ok(job) = rx.recv() {
                println!("執行緒 {} 處理工作: {}", id, job);
                // 模擬工作
                thread::sleep(std::time::Duration::from_millis(200));
            }
        });
        workers.push(worker);
    }

    // 發送 10 個工作項目
    for i in 0..10 {
        tx.send(format!("工作 {}", i)).unwrap();
    }
    drop(tx); // 關閉通道,讓工作者結束迴圈

    for w in workers {
        w.join().unwrap();
    }
}
  • 這裡使用 mpsc(multiple producer, single consumer)通道在主執行緒與工作執行緒之間傳遞任務。
  • 真正的專案建議使用 rayontokioasync-std 等成熟庫,本文僅示範概念。

常見陷阱與最佳實踐

陷阱 可能的結果 解決方式
忘記 move 編譯錯誤:閉包捕獲的變數無法跨執行緒使用 spawn 的閉包前加 move,或使用 Arc/Mutex
共享可變資料未加鎖 資料競爭、未定義行為、程式 panic 使用 Arc<Mutex<T>>RwLock,確保同時只有一個執行緒寫入
過度產生執行緒 系統資源耗盡、效能下降 使用執行緒池或限制同時執行的執行緒數量
忽略 join 的錯誤 子執行緒 panic 但主執行緒仍繼續,可能導致不一致狀態 使用 handle.join().expect("子執行緒失敗"),或自行處理 Result
跨執行緒傳遞非 Send 類型 編譯錯誤:T 必須實作 Send 確認要傳遞的資料類型實作 Send(大多數標準型別已支援)

最佳實踐

  1. 盡量使用 move + Arc/Mutex:保證所有權清晰,避免隱藏的資料競爭。
  2. 限制執行緒數量:根據 CPU 核心數 (num_cpus::get()) 設定上限,或使用成熟的執行緒池庫。
  3. 錯誤處理join() 必須檢查 Result,避免 silent panic。
  4. 避免長時間阻塞:若執行緒內部有 I/O,考慮使用非同步 runtime(如 tokio)取代傳統執行緒。
  5. 測試與 profiling:使用 cargo benchperfflamegraph 觀察執行緒的實際效能。

實際應用場景

場景 為何使用 spawn 範例簡述
網路爬蟲 同時抓取多個網站,提升下載速度 為每個 URL 建立子執行緒,使用 join 收集結果
圖像處理 大量像素運算可平行化 把圖像切割成多塊,交給不同執行緒計算,最後合併
日誌寫入 高頻率寫入不阻塞主流程 建立單一寫入執行緒,主執行緒透過 channel 發送 log 訊息
資料庫批次寫入 多筆寫入同時進行,減少等待時間 使用執行緒池分配寫入工作,提升吞吐量
遊戲伺服器 處理多玩家的即時指令 每個玩家的指令交給獨立執行緒或工作池,保持低延遲

小技巧:在 CPU 密集型工作時,thread::available_parallelism()(Rust 1.59+)可取得系統建議的平行度,作為建立執行緒數量的參考。


總結

  • std::thread::spawn 是 Rust 建立執行緒的核心 API,配合 moveArcMutex 能安全地在多執行緒環境中共享與修改資料。
  • 正確處理所有權、錯誤與資源限制,是避免競爭條件與效能瓶頸的關鍵。
  • 實務上,執行緒池非同步 runtime適當的同步原語 結合,能讓程式在多核心機器上發揮最大效能。
  • 只要掌握上述概念與最佳實踐,從簡單的 spawn 到複雜的並行演算法,Rust 都能提供「零成本抽象」的安全保證。

祝你在 Rust 的並行世界中寫出高效、可靠的程式碼! 🚀