Runloom: 为自由线程 Python 3.13t+ 提供 Go 风格的协程

Runloom: 为自由线程 Python 3.13t+ 提供 Go 风格的协程

Runloom 是一个高性能的 Python 运行时,它实现了 Go 风格的有栈协程(fibers),使开发者能够编写阻塞风格的代码,并在单个进程中跨所有可用的 CPU 核心进行扩展。通过利用自由线程化的 CPython(3.13t 和 3.14t),Runloom 消除了全局解释器锁(GIL)的瓶颈,允许工作窃取调度器将数百万个 fiber 分布到多个原生线程中。

高性能 M:N 调度

Runloom 实现了一种 M:N 工作窃取调度器,将许多 fiber (M) 映射到较少数量的原生 hub 线程 (N) 上。这种架构允许 Python 应用程序在无需 OS 线程开销或 async/await 语法复杂性的情况下实现大规模并发。

关键技术组件包括:

  • 手工编写的汇编上下文切换:使用 x86_64 SysV 和 aarch64 汇编进行上下文切换(约 80 ns/次切换),无需系统调用,并提供 Windows Fibers 或 POSIX ucontext 作为回退方案。
  • 工作窃取架构:每个 hub 使用 Chase-Lev deque,并采用每 hub MPSC 提交方式,以平衡各核心间的负载。
  • PyThreadState 快照:为每个 fiber 捕获 CPython eval frame、数据栈、异常信息和上下文变量,防止在数百万个 fiber 被挂起时出现“frame-chain cliff”。
  • Netpoll 集成:一个跨平台的轮询系统(epoll, kqueue, IOCP, WSAPoll, select),可以在文件描述符就绪时透明地挂起 fiber。
  • 停顿隔离 (Stall Isolation):运行时可以检测并从导致特定 hub 线程停顿的意外阻塞调用中恢复,确保系统的其余部分保持响应。

与 Go 的性能基准测试对比

在 64 核机器上使用自由线程化 CPython 3.13t 进行的基准测试显示,Runloom 在几个关键领域表现出与 Go 相当的性能,尽管它保持了较高的内存占用。

指标 Runloom Go 结论
纯 C 启动 (Pure C Spawn) 2.29 M/s 2.10 M/s 优于 Go
Python 启动 (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× (持平)
内存 (空 Fiber) 8.8 KB 2.7 KB 3.3× 差距

虽然 Runloom 在吞吐量和调度速度方面与 Go 相当,但主要的权衡在于内存;由于 CPython eval frame 的要求,挂起的 fiber 需要大约是 Go goroutine 内存的 3.3 倍。

开发者体验与 API

Runloom 允许开发者通过使用同步 API 和一种使标准库阻塞调用变为协作式的猴子补丁(monkey-patching)系统,从而避免使用 async/await

同步 Fiber API

开发者可以使用 runloom.fiber(fn) 启动 fiber,并通过 Go 风格的通道 (Chan, select) 进行通信。

import threading, runloom
from urllib.request import urlopen
runloom.monkey.patch()

def crawl(url):
    # 猴子补丁使此阻塞调用变为协作式
    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) # 在 8 个 hub 线程上跨真实核心运行

Asyncio 桥接

对于现有的异步代码,runloom.aio 提供了一个桥接器,可以在单线程调度器上运行 async def 函数,从而提供了一条无需完整重写代码库的迁移路径。

限制与要求

为了实现多核并行,Runloom 需要 自由线程化 CPython 3.13t 或 3.14t。在标准的启用 GIL 的构建版本中,Runloom 仍然可以提供廉价的 fiber 启动和 netpoll,但它仍受限于单个 CPU 核心,类似于 asyncio

其他约束包括:

  • 字节码抢占:抢占仅发生在 Python 字节码边界处。在纯 C 扩展(如 NumPy)中执行紧密循环的 fiber 会持有其 hub 线程,直到 C 调用返回。
  • 内存开销:如基准测试所示,fibers 消耗的内存比 Go 的 goroutines 更多。
  • 平台验证:虽然支持 Linux, macOS, Windows, 和 FreeBSD,但 Linux x86_64 在 3.13t 上的表现是主要且经过最严格验证的目标平台。

社区观点

来自开发者社区的早期反应突出了技术成就,同时也提醒了需要对新的、复杂的运行时进行谨慎对待。一些用户指出,该项目的规模——包含超过 12,000 行 C 代码和 240,000 行 Python 代码——在单个 commit 中引入如此大量的代码是一个重大挑战,引发了对项目初始信任度和稳定性的疑问。其他人则将其与 gevent 进行比较,并指出通过移除 GIL 实现的 M:N 工作窃取模型使其成为对以往协作式多任务库的一次重大演进。

Sources