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 工作窃取模型使其成为对以往协作式多任务库的一次重大演进。