Runloom: 為自由執行緒 Python 3.13t+ 提供 Go 風格的協程
Runloom: 為自由執行緒 Python 3.13t+ 提供 Go 風格的協程
Runloom 是一個高效能的 Python 執行階段,它實現了 Go 風格的堆疊式協程 (fibers),讓開發者能夠編寫阻塞式風格的程式碼,並在單一程序中跨所有可用的 CPU 核心進行擴展。透過利用自由執行緒 CPython (3.13t 與 3.14t),Runloom 消除了全域解釋器鎖 (GIL) 的瓶頸,允許工作竊取調度器 (work-stealing scheduler) 將數百萬個 fibers 分佈到多個原生執行緒上。
高效能 M:N 調度
Runloom 實現了 M:N 工作竊取調度器,將許多 fibers (M) 映射到較少數量的原生 hub threads (N)。這種架構允許 Python 應用程式在無需 OS threads 的開銷或 async/await 語法的複雜性下,實現大規模並行。
關鍵技術組件包括:
- 手寫組合語言上下文切換 (Hand-rolled Assembly Context Switching):使用 x86_64 SysV 與 aarch64 組合語言進行上下文切換(每次切換約 ~80 ns),無需系統呼叫,並提供 Windows Fibers 或 POSIX
ucontext作為備選方案。 - 工作竊取架構 (Work-Stealing Architecture):每個 hub 採用 Chase-Lev deque,並透過每 hub MPSC 提交來平衡各核心間的負載。
- PyThreadState 快照:為每個 fiber 擷取 CPython eval frame、數據堆疊、異常資訊與上下文變數,防止在數百萬個 fibers 被讓出時發生「frame-chain cliff」。
- Netpoll 整合:一個跨平台的輪詢系統 (epoll, kqueue, IOCP, WSAPoll, select),能透明地在檔案描述符就緒時暫停 fibers。
- 停滯隔離 (Stall Isolation):執行階段可以偵測並從意外的阻塞呼叫中恢復,這些呼叫會導致特定的 hub thread 停滯,從而確保系統其餘部分保持響應。
與 Go 的效能基準測試
在 64 核心機器上使用自由執行緒 CPython 3.13t 進行的基準測試顯示,Runloom 在幾個關鍵領域展現了與 Go 相當的效能,儘管其記憶體佔用較高。
| 指標 | Runloom | Go | 結論 |
|---|---|---|---|
| Pure C Spawn | 2.29 M/s | 2.10 M/s | 勝過 Go |
| Python Spawn | 1.35 M/s | 2.10 M/s | 0.65× |
| Context Switch | ~75ns yield / ~560ns chan | ~50ns Gosched |
相當 |
| Conn/s (Churn) | ~75–78 k/s | ~75–78 k/s | 相當 |
| Req/s (Keep-alive) | 596 k/s | 603 k/s | 0.99× (相當) |
| Memory (Empty Fiber) | 8.8 KB | 2.7 KB | 3.3× 差距 |
雖然 Runloom 在吞吐量與調度速度上與 Go 相當,但主要的權衡是在於記憶體;由於 CPython eval frame 的需求,一個暫停中的 fiber 需要大約是 Go goroutine 的 3.3 倍記憶體。
開發者體驗與 API
Runloom 允許開發者避免使用 async/await,透過使用同步 API 與一個 monkey-patching 系統,讓標準函式庫的阻塞呼叫變為協作式。
同步 Fiber API
開發者可以使用 runloom.fiber(fn) 來產生 fibers,並透過 Go 風格的 channels (Chan, select) 進行通訊。
import threading, runloom
from urllib.request import urlopen
runloom.monkey.patch()
def crawl(url):
# Monkey-patching 讓此阻塞呼叫變為協作式
body = urlopen(url, timeout=10).read()
print(threading.get_native_id(), len(body))
def main():
for _ in range(64):
runloom.fiber(crawl, "http://example.com")
runloom.run(8, main) # Runs on 8 hub threads across real cores
Asyncio Bridge
對於現有的非同步程式碼,runloom.aio 提供了一個橋接器,可以在單執行緒調度器上執行 async def 函數,提供了一條無需完整重寫程式碼庫的遷移路徑。
限制與需求
為了實現多核心並行,Runloom 需要 free-threaded CPython 3.13t 或 3.14t。在標準的 GIL-enabled builds 中,Runloom 仍能提供廉價的 fiber 產生與 netpoll,但仍受限於單一 CPU 核心,類似於 asyncio。
其他限制包括:
- 位元組碼搶佔 (Bytecode Preemption):搶佔僅發生在 Python 位元組碼邊界處。在純 C 擴展(如 NumPy)中執行緊密迴圈的 fiber 會佔用其 hub thread 直到 C 呼叫返回。
- 記憶體開銷 (Memory Overhead):如基準測試所示,fibers 消耗的記憶體比 Go 的 goroutines 更多。
- 平台驗證:雖然支援 Linux, macOS, Windows, 與 FreeBSD,但 Linux x86_64 上的 3.13t 是主要且經過最嚴格驗證的目標平台。
社群觀點
開發者社群的早期反應強調了技術成就,但也對新的、複雜的執行階段提出了謹慎態度。一些使用者指出,該專案的規模——包含超過 12,000 行 C 與 240,000 行 Python——在單一 commit 中引入如此大量的程式碼,對於初始信任與穩定性提出了疑問。其他人則將其與 gevent 進行了有利的比較,指出透過移除 GIL 所實現的 M:N 工作竊取模型,使其成為對先前協作式多工函式庫的重大演進。