Runloom: Free-Threaded Python 3.13t+ 用の Go スタイルのコルーチン
Runloom: Free-Threaded Python 3.13t+ 用の Go スタイルのコルーチン
Runloom は、Go スタイルのスタックフル・コルーチン(ファイバー)を実装する Python 用の高パフォーマンス・ランタイムです。これにより、開発者はブロッキング形式のコードを記述し、単一プロセス内で利用可能なすべての CPU コアにスケールさせることが可能になります。Free-threaded CPython (3.13t および 3.14t) を活用することで、Runloom は Global Interpreter Lock (GIL) のボトルネックを解消し、ワークスティーリング・スケジューラによって数百万のファイバーを複数のネイティブ・スレッドに分散させることができます。
高パフォーマンスな M:N スケジューリング
Runloom は、多数のファイバー (M) を少数のネイティブ・ハブ・スレッド (N) にマッピングする M:N ワークスティーリング・スケジューラを実装しています。このアーキテクチャにより、Python アプリケーションは、OS スレッドのオーバーヘッドや async/await 構文の複雑さなしに、大規模な並行性を実現できます。
主な技術的コンポーネントは以下の通りです:
- Hand-rolled Assembly Context Switching: x86_64 SysV および aarch64 アセンブリを使用して、システムコールなしでコンテキスト・スワップを実行します(約 80 ns/swap)。Windows Fibers または POSIX
ucontextへのフォールバック機能も備えています。 - Work-Stealing Architecture: 各ハブごとに Chase-Lev deque を使用し、ハブごとの MPSC 投入により、コア間で負荷を分散させます。
- PyThreadState Snapshots: 各ファイバーに対して CPython の eval frame、データ・スタック、例外情報、およびコンテキスト変数(context variables)をキャプチャし、数百万のファイバーが yield される際の「frame-chain cliff」を防ぎます。
- Netpoll Integration: ファイル記述子の準備状態に応じてファイバーを透過的に待機させる、クロスプラットフォームのポーリング・システム(epoll, kqueue, IOCP, WSAPoll, select)です。
- Stall Isolation: ランタイムは、特定のハブ・スレッドを停止させる予期せぬブロッキング呼び出しを検出し、そこから回復することができます。これにより、システムの他の部分の応答性を維持します。
Go とのパフォーマンス・ベンチマーク比較
Free-threaded CPython 3.13t を使用した 64 コア・マシンでのベンチマークでは、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 の要件により、中断されたファイバーは Go の goroutine の約 3.3 倍のメモリを必要とします。
開発者体験と API
Runloom は、同期的な API と、標準ライブラリのブロッキング呼び出しを協力的(cooperative)にするモンキーパッチ・システムを使用することで、開発者が async/await を回避できるようにします。
同期ファイバー API
開発者は runloom.fiber(fn) を使用してファイバーを生成し、Go スタイルのチャネル (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 有効ビルドでは、Runloom は安価なファイバー生成と netpoll を提供しますが、asyncio と同様に単一 CPU コアに制限されます。
その他の制約は以下の通りです:
- Bytecode Preemption: プリエンプションは Python バイトコードの境界でのみ発生します。NumPy のような純粋な C 拡張モジュールでタイトなループを実行しているファイバーは、C の呼び出しが戻るまでハブ・スレッドを保持し続けます。
- Memory Overhead: ベンチマークで述べたように、ファイバーは Go の goroutine よりも多くのメモリを消費します。
- Platform Validation: Linux, macOS, Windows, および FreeBSD がサポートされていますが、Linux x86_64 on 3.13t が主要かつ最も検証されているターゲットです。
コミュニティの視点
開発者コミュニティからの初期の反応は、技術的な達成と、新しい、複雑なランタイムに対する慎重な姿勢の両方を強調しています。プロジェクトの規模(C 言語 12,000 行以上、Python 240,000 行以上)が、単一のコミットで導入される膨大な量であり、初期の信頼性と安定性に関する疑問を疑問視する声もあります。一方で、他のユーザーは gevent と比較して、GIL の除去によって可能になった M:N ワークスティーリング・モデルが、従来の協力的マルチタスク・ライブラリの大きな進化であると述べています。