「桌上同時只有一張嘴有權出聲,這不是禮貌,是一把鎖。輪到你才是你的,插話的話從來不算數。」
——《阿帕契開源審計錄》¹ 卷二·絕發篇
幕間
三號質問二號昨天白天一句有用的話都沒說。
二號覆誦:「我沒講有用的話……因為急著表態的才可疑。」
敘事者連忙勸解。他不知道自己正替一個永遠不先表態的人,補上合理的藉口。
六號講到一半,九號忍不住插了一句——才半句,法官的木槌就敲下來:「絕發。輪次不在你,收回。」九號的話當場作廢,像沒說過。全場安靜,炭筆聲、呼吸聲都聽得見,就是沒有第二個人的聲音。
這條規則我玩了五世,第一次認真想它到底在保護什麼。桌上九個人,腦子都在轉,可是同一個時刻只有一個人能把想法變成「發言」寫進這一輪的紀錄;其他八個人的思考再激烈,沒輪到就是不存在。一張嘴,一個權杖,順著座位傳。
七號今晚問問題的方式變了。以前她問「你覺得誰是狼」,今晚她問二號:「你為什麼想投六號?」二號愣了一下,說「我同意四號剛才講的,六號的票太跳」。七號在羊皮紙上記的不是「二號投六號」,是「二號:附議四號」。她開始要理由,不要結論。
一張嘴輪流傳,這條規則讓村莊的發言有秩序;但它也讓某個從不搶著開口的人,永遠不必先攤出自己的打算。這是保護,還是掩護?
CPython 的 GIL 是一把直譯器層級的鎖:同一時刻只有握著它的那條執行緒能執行 Python 位元組碼。執行緒確實存在,作業系統也在排它們,但它們是輪流上 GIL——每隔幾毫秒、或一遇到 I/O 就交出去。所以多執行緒的純 Python 程式不會在多核心上真的平行跑,只是交錯著併發。這正是絕發:九條思緒,一個發言權杖,順著桌子傳。
它保護的東西很具體。CPython 每個物件都有一個參考計數欄位 ob_refcnt,而 Py_INCREF / Py_DECREF 發生的頻率高得驚人——光是把一個變數綁到另一個名字、把物件放進一個 list、當成參數傳進函式,都會動到它。要是沒有 GIL,這每一次加減都得換成原子操作或各自上鎖,單執行緒效能會被拖垮,而且幾十年來的 C 擴充套件全都假設「我在跑的時候沒有別的執行緒動 Python 物件」。GIL 用一把大鎖換掉了無數把小鎖,代價就是多核心的平行被犧牲掉。這是執行期的仲裁:程式一邊跑,鎖一邊決定誰能動。
asyncio 連執行緒都不用——單一執行緒,一個事件迴圈。協程一路跑到 await 才把控制權交還迴圈,迴圈用 selector 檢查哪些檔案描述符就緒,再喚醒對應的協程。這是合作式排程:一個協程若從不 await,或在裡面做了阻塞的 CPU 工作,整個迴圈就被它一個人卡死,就像輪到自己卻不肯停下來的玩家。它擅長同時照顧大量 I/O 密集的任務,但對 CPU 平行毫無幫助——還是一條執行緒,還是 GIL。
CPython 3.13 起附了一個實驗性的自由執行緒建置版本,用偏向式參考計數、部分不朽物件與逐物件鎖把 GIL 拿掉,讓純 Python 也能真正跨執行緒平行。但這不是免費的:單執行緒會有個位數百分比的效能退步(正在收斂),大量 C 擴充套件得逐一移植才安全,所以到 2026 年初它仍是選用、非預設。換句話說,絕發這條規則短期內還撤不掉,CPython 只是開始準備一套「同時可以有多人發言」的替代秩序。
Rust 沒有 GIL。它用兩個標記 trait,由編譯器依欄位自動推導:Send 表示這個型別的值可以安全地「移動」到另一條執行緒;Sync 表示它可以安全地以 &T 參照被多條執行緒「共享」,形式上 T: Sync 等價於 &T: Send。舉例:Rc<T> 的參考計數是非原子的,兩條執行緒同時複製一份就會把計數改壞,所以它 !Send 也 !Sync,編譯器直接拒絕你把它丟進 thread::spawn;RefCell<T> 的借用旗標同樣沒有同步,它 Send 但 !Sync——可以整個搬過去,不能兩邊同時借。對照組是 Arc<T>(當 T: Send + Sync 時,Arc<T> 本身也是 Send + Sync)與 Mutex<T>(只要 T: Send,它就具備 Sync,因為內部互斥鎖保護了並行存取)——這就是為什麼不能單純把 RefCell<T> 包進 Arc 就拿去跨執行緒,編譯器一樣會擋下來。把一個 &mut 別名跨執行緒共享,借用檢查器在更前面就擋掉了。全部發生在編譯期:資料競爭是建置錯誤,不是上線後才炸的 bug。執行期不需要一把鎖來仲裁,因為這個座位安排在程式開始跑之前就被證明是安全的。真的需要共享可變狀態時就用 Arc<Mutex<T>>,而編譯器會逼你先 lock() 拿到 guard 才碰得到裡面的值。
tokio 預設的執行期是多執行緒的,工作竊取排程,把 async 任務散到所有核心上真的平行跑。這裡 Send 的約束就派上用場了:一個任務在某次 .await 之後被喚醒時,可能是由另一條工作執行緒接手繼續 poll,於是這個 future 跨越 .await 所捕捉的所有區域狀態都必須是 Send,否則編譯不過。如果你不需要跨核心平行,想在單執行緒處理 !Send 的 future,得改用 tokio::task::LocalSet 配合 spawn_local,否則一般的 tokio::spawn 不管執行期是不是 current_thread,型別簽章上依然要求 Send。於是 Rust 同時拿到 asyncio 那套 async / .await 的手感,又拿到真正的平行——Send / Sync 的約束正是讓這件事安全的關鍵。DataFusion 的查詢執行就建在 tokio 上,能把一個查詢計畫切成多個分割在多核心同時算;Airflow 的排程器則是一堆 Python 行程繞著 GIL 工作,靠多行程而非多執行緒取得平行。
編譯期攔截與執行期輪詢的差別,一句話說完:Rust 在程式開始跑之前就把不安全的共享組合擋在門外,跑起來完全沒有為了安全而付的執行期成本;Python 則是讓程式先跑,再靠 GIL(多執行緒)或事件迴圈(asyncio)在執行途中決定此刻誰能動。
use std::rc::Rc;
use std::sync::Arc;
use std::thread;
fn main() {
// Rc is NOT Send: a non-atomic reference count would race across threads.
let table_notes = Rc::new(vec!["seat-2 seconds seat-4"]);
// thread::spawn(move || {
// println!("{:?}", table_notes); // compile error: `Rc<...>` cannot be sent between threads safely
// });
let _ = table_notes;
// Arc IS Send + Sync: the compiler now lets the value cross the thread boundary.
let audit_log = Arc::new(vec!["night-1", "night-2", "night-3"]);
let mut handles = Vec::new();
for id in 0..3 {
let log = Arc::clone(&audit_log);
handles.push(thread::spawn(move || {
println!("worker {id} reads {} entries", log.len());
}));
}
for handle in handles {
handle.join().unwrap();
}
}
import asyncio
async def question(seat: str, delay: float) -> str:
await asyncio.sleep(delay) # yields the single thread back to the loop
return f"{seat} answered"
async def main() -> None:
# Concurrent, not parallel: one thread, the event loop interleaves them.
results = await asyncio.gather(
question("seat-2", 0.2),
question("seat-4", 0.1),
)
print(results)
asyncio.run(main())
Rust 那段把危險的組合擋在編譯期:Rc 想跨執行緒,程式根本編不過;Arc 可以,於是三條執行緒平行讀同一份稽核紀錄,完全不需要執行期的鎖來看守。Python 那段是合作式的併發:兩個協程在同一條執行緒上,靠事件迴圈交錯,await 是它們交出權杖的時刻。

九號那句被木槌打掉的插話,天亮後沒有任何人記得內容——夜間死亡沒有遺言,被規則作廢的發言也一樣,安靜地不存在。絕發是一把鎖,我玩了五世才想問:鎖總是為了關住什麼才上的。是誰,在這張桌子還沒發牌之前,就決定把某個東西關進這一局跑不到終點的黑夜裡?絕發保護每個人把一段話完整說完的權利,這是它好的一面;代價是,只要輪到你時你永遠接一句「我同意剛才那位」,你就不必先攤牌。二號今晚又是這樣:七號問他為什麼投六號,他先把票投向四號指的方向,再補一句附議。她沒有追問,只是在他那一行的理由欄寫下「附議四號」,跟前面幾晚一樣的四個字。她要的還只是理由本身。
你現在該能:講清楚 GIL 為什麼讓多執行緒的純 Python 程式無法真正平行,以及 asyncio 的事件迴圈跟多執行緒是兩回事;說出 Rust 的 Send 與 Sync 各自保證什麼,為什麼 Rc 不能跨執行緒而 Arc 可以。在東西上線出事之前,就先聞出一個 &mut 別名會跨執行緒洩漏、把它擋在編譯期門外——這種對「借用衝突」的鼻子,得親手跟借用檢查器纏鬥夠久才長得出來,像練成一項青銅般的硬本事。想動手,就照 Rust 官方 Book 的「Fearless Concurrency」那章把上面的編譯錯誤重現一次,再用 Python 寫一段 asyncio,故意在某個協程裡放一個阻塞呼叫,看整個迴圈怎麼被拖垮。
Send trait
Sync trait
asyncio 協程與 Task
Python GIL bytecode interpreter lock free-threaded build asyncio event loop selectors cooperative scheduling Rust Send Sync marker traits fearless concurrency tokio work-stealing multi-thread runtime
¹ 註:本書名為情境設定之虛構文獻,非真實歷史或開源紀錄。