seansie's blog

Ch7:併發、隧道與巨集 — 跨時空的對話

· Sean Sie

併發 (Concurrency):安全的多執行緒

Rust 是目前唯一敢號稱「無畏併發」(Fearless Concurrency)的語言。為什麼?因為在其他語言中,併發像是走鋼絲,稍微不注意就會發生「資料競爭」(Data Race)。但在 Rust,它的所有權系統借用檢查器直接在編譯階段就幫你把這些隱患掐死了.

thread::spawn:開啟平行時空

在 Rust 開啟一個新執行緒非常簡單,但你會遇到第一個門檻:move 閉包.

use std::thread;

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

    let handle = thread::spawn(move || {  
        println!("在子執行緒中使用 vector: {:?}", v);  
    });

    // v 在這裡已經不能用了,因為所有權已經 move 進去了  
    handle.join().unwrap(); // 等待子執行緒跑完,這叫「同步」  
}

極速講解

  • move 的必要性:Rust 無法確定子執行緒會活多久. 如果沒有 move ,子執行緒可能在主執行緒銷毀 v 之後還在嘗試讀取它. 為了安全,Rust 強制要求把所有權「打包帶走」 .
  • JoinHandlespawn 會回傳一個 handle . 如果你不 join ,主執行緒結束時,子執行緒會直接被強制關閉(像被拔掉電源一樣) .

thread::scope:借用大解放

如果你覺得 move 太暴力,想要在子執行緒裡「借用」主執行緒的東西怎麼辦?傳統 spawn 會報錯. 這時候就要出動 scope 了.

thread::scope(|s| {  
    s.spawn(|| {  
        println!("我可以借用主執行緒的 v: {:?}", &v);  
    });  
    // scope 結束前,保證所有 s.spawn 跑完,所以借用是安全的!  
});

💡 深度分析scope 建立了一個保險箱環境,它保證在閉包結束前所有執行緒都會歸位. 編譯器知道這點,所以它才敢放心地讓你進行非靜態(non-static)的借用.

隧道 (Channel):訊息傳遞

Rust 的併發哲學深受 Go 語言影響:「不要透過共享記憶體來通訊,要透過通訊來共享記憶體。」這就是 mpsc 隧道(通道).

  • mpsc 代表什麼?:Multi-Producer, Single-Consumer(多生產者,單消費者). 你可以複製很多個發送端(tx),但接收端(rx)只能有一個.
use std::sync::mpsc;  
use std::thread;

let (tx, rx) = mpsc::channel();  
let tx1 = tx.clone(); // 複製發送端,現在有兩個人可以傳訊息

thread::spawn(move || {  
    tx.send("來自發送端 A 的問候").unwrap();  
});

thread::spawn(move || {  
    tx1.send("來自發送端 B 的反擊").unwrap();  
});

for received in rx {  
    println!("收到訊息: {}", received);  
}

生存指南

  • 阻塞與非阻塞rx.recv() 會一直等(阻塞),直到收到訊息. 如果你不想等,可以用 try_recv() ,沒東西它會立刻回傳 Err .
  • 隧道的關閉:當所有發送端都銷毀(drop)後,接收端的 for 迴圈就會自動停止.

原子操作與鎖:共享狀態的交通管制

當你真的需要多個人同時改同一個東西時,你需要強大的同步工具.

Arc與原子操作

Rc 在多執行緒下會崩潰,你需要 Arc (Atomic Reference Counting). 而對於基本型態(如 usize ),可以直接用 std::sync::atomic ,這是硬體等級的互斥鎖,效能極高.

Mutex 與 RwLock

  • Mutex:互斥鎖. 想拿資料?先敲門拿 Lock. 拿不到就排隊.
  • RwLock:讀寫鎖. 支援「多人同時看」或是「一人專心改」. 如果你的資料是「多讀少寫」,這比 Mutex 快得多.

系統函數:與作業系統的硬核對話

Rust 提供了一套極其強大且嚴謹的作業系統 API. 因為這些操作可能因為權限、空間等問題失敗,所以它們的回傳值幾乎都是 Result .

1. 檔案管理 (File System)

使用 std::fs::File . 你可以像在玩積木一樣設定檔案權限.

  • 基礎開檔File::create(path) (新建) 或 File::open(path) (唯讀開啟) .
  • 進階選項 (OpenOptions):這是建造者模式的展現.
  use std::fs::OpenOptions;  
  let file = OpenOptions::new()  
      .read(true)  
      .append(true)  
      .create(true) // 檔案不存在就建一個  
      .open("log.txt")?;
  • 快速讀寫:Rust 提供了一鍵完成的 API:
    • fs::write("file.txt", "內容")? :快速寫入.
    • fs::read_to_string("file.txt")? :直接讀成字串.
  • 緩衝讀取 (BufReader):如果你要讀超大檔案,請務必用 BufReader ,它會幫你預讀,避免頻繁請求作業系統,效能差好幾倍.

2. 行程管理 (Process Management)

這讓你可以操控作業系統中的其他程式,像是命令列工具.

  • Command 建造者
  use std::process::{Command, Stdio};  
  let output = Command::new("ls") // 啟動 ls 指令  
      .arg("-l") // 帶參數  
      .current_dir("/") // 設定工作路徑  
      .output()?; // 阻塞並「收割」所有結果
  • 三大「收割」模式
    • .spawn() :放手模式. 執行後不管它,回傳一個 Child 句柄,你可以之後再殺掉( .kill() )或等待它.
    • .status() :等待模式. 父行程會停下來,直到子行程跑完,只關心最後成功沒.
    • .output() :全面模式. 阻塞並把所有輸出的內容(stdout/stderr)抓回記憶體.

3. 網路系統 (Networking)

std::net 分成三個層次:位址表示、TCP、以及 UDP.

  • IP 與地址IpAddr (v4/v6) 與 SocketAddr (IP + Port).
  • TCP 伺服器
  use std::net::TcpListener;  
  let listener = TcpListener::bind("127.0.0.1:8080")?;  
  for stream in listener.incoming() { // 像水龍頭一樣持續接收連線  
      let mut stream = stream?;  
      // 讀取與寫入操作...  
  }
  • UDP Socket:不分 ListenerStream ,統一用 UdpSocket .

4. 時間處理 (Time)

  • Instant:碼錶. 單調遞增,適合用來計時(計算函數跑多快). 不受系統調時影響.
  • Duration:時間段. 比如「5 秒鐘」.
  • SystemTime:掛鐘. 與電腦的時間同步,適合紀錄「檔案創立日期」.

巨集 (Macro):代碼的代碼

巨集是 Rust 的黑魔法,本質上是「寫一段程式碼來幫你寫程式碼」.

1. 宣告式巨集 (macro_rules!)

像模式匹配一樣,把重複的代碼模組化. 例如一個自定義的 map! 巨集.

2. 衍生巨集 (Derive)

這是最常用的黑魔法. 只要在結構體加上 #[derive(Debug, Clone)] ,編譯器就會自動幫你寫完那些無聊的介面實作.

3. 屬性巨集與函數式巨集

這類巨集像 Python 的裝飾器,能直接修改或生成函數體. 著名的 serdetokio 都是靠這個撐起來的.

💡 溫馨提醒:巨集雖然爽,但它會增加編譯時間,且錯誤訊息有時候像天書. 如果普通函數能解決,請優先使用函數.

總結:Ch7 是 Rust 展現硬實力的章節. 併發保證安全,巨集保證簡潔,系統 API 保證效能. 學會這章,你才算真正踏入 Rust 的高階領域!