ADR-005 — 内部并发原语:Python asyncio(结构化并发 TaskGroup)
状态 = draft(formalize 架构 §12 已述倾向,待 owner 签)。里程碑归属:M0 已用 asyncio 基础;业务层 TaskGroup 并发在 M2 任务类型扩展 兑现。本 ADR 不阻塞 M1。
§0 决策简述
FinBayes Runtime 以 Python asyncio 为基础并发原语,业务对象 TaskGroup 映射 asyncio.TaskGroup(Python 3.11+ 结构化并发)。CPU 密集操作(本地推理 / 大量解析)经 loop.run_in_executor 抛线程/进程池,不阻塞事件循环。
§1 上下文(架构 §12)
- Runtime 工作是 I/O 密集(LLM 调用 / 工具调用 / 数据 API 都是网络 I/O),非 CPU 密集。
- 业务对象 TaskGroup(一个用户输入命中多任务时的并发容器,§11.3)需要结构化并发原语承接其失败隔离 / 取消 / 超时语义。
§2 决策与备选
| 方案 | 评价 |
|---|---|
| ✅ asyncio + TaskGroup | 单线程事件循环避免锁竞争/死锁;与 httpx/aiohttp/FastAPI 生态契合;3.11+ 结构化并发匹配业务 TaskGroup(父任务统一管理子任务生命周期/取消) |
| ❌ 多线程 | I/O 场景被 GIL 限制收益有限;锁/死锁坑多;失败隔离与取消语义比 TaskGroup 难表达 |
| ❌ 多进程 | IPC + 序列化开销重;单机认知层 MVP 不需要真并行 CPU;过度工程 |
例外:CPU 密集(本地 LLM 推理 / 大量 PDF 解析)→ run_in_executor 到线程池/进程池。
§3 后果
- 业务 TaskGroup →
asyncio.TaskGroup:子任务失败隔离(一个失败不阻塞其他)、用户取消向子任务传播 cancel、超时经asyncio.timeout。 - 阻塞调用必须包 executor,否则卡事件循环(§12 测试约束需有断言)。
- 测试用 pytest-asyncio / anyio。
§4 里程碑
- M0:asyncio 基础(
asyncio.TaskGroup基础)已在骨架。 - M2:业务 TaskGroup 多任务并行 + 失败隔离 + 取消/超时语义完整落地(本 ADR 的主要兑现点)。
§5 关联
- 架构 §12 并发与异步处理 / §11.3 TaskGroup 生命周期。
- ADR-008 LLM Provider 接口抽象(provider 调用是主要 I/O await 点)。