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.aioasync 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 ワークスティーリング・モデルが、従来の協力的マルチタスク・ライブラリの大きな進化であると述べています。

Sources