RFC-008:Runtime 并发模型与调度器脱耦设计
⚠️ 对齐说明:本文档已与 RFC-024 新并发模型 对齐。旧的全程序 DAG 分析、
@block/@eager注解、L1/L2/L3 层级模型已被spawn {}块并行原语取代。DAG 分析现在仅作用于spawn {}块内部。
参考:
摘要
本文档定义 Runtime 架构的关键设计:
- 运行时三层架构:Embedded(即时执行) → Standard(spawn + DAG 调度) → Full(工作窃取)
- 编译与运行分离:编译阶段完全相同,区别仅在运行时执行方式
- 双后端模型:VM(开发调试)与 LLVM AOT(生产发布),行为完全一致
- 调度器 = 静态库:AOT 编译时调度器链接进 exe,约 200-500KB,无 GC
- 同步只是调度的特例:num_workers=1 即同步模式
关键澄清:这不是 Java
Java: .java → .class → JVM(解释/JIT + GC) ← 永远需要虚拟机
YaoXiang 开发: .yx → IR → VM 执行(快速迭代、步进调试)
YaoXiang 生产: .yx → IR → LLVM → 原生 exe(调度器链接进去)
VM 是开发工具,不是运行时本质。跟 Go 的 go run vs go build 一样。
最终 exe = 你的原生代码 + 调度器静态库 + 反射元数据。没有解释器,没有 JIT,没有 GC。动机
核心矛盾
| 矛盾 | 描述 |
|---|---|
| 透明性 vs 可控性 | spawn 块提供显式并发控制,普通代码顺序执行 |
| 核心 vs 可选 | spawn 是核心并行原语,WorkStealing 是 num_workers>1 的高级特性 |
| 单线程 vs 并发 | 单线程模式下并发表现为异步,同步只是调度的特例 |
提案
1. Runtime 三层架构
┌──────────────────────────────────────────────────────────────────┐
│ 编译阶段(所有模式相同) │
│ │
│ Source Code → Lexer → Parser → TypeCheck → Codegen → IR │
│ │
│ ⚠️ 同一套语法解析、类型检查、代码生成、IR 输出 │
└──────────────────────────────────────────────────────────────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌───────────────┐ ┌──────────────────┐
│ 🟢 Embedded │ │ 🔵 Standard │ │ 🟣 Full │
│ 即时执行器 │ │ spawn + DAG │ │ Full 调度器 │
│ 同步执行 │ │ spawn 块内并发 │ │ 并行优化 │
│ 无 spawn 支持 │ │ 自动并发 │ │ 工作窃取 │
└──────────────────┘ └───────────────┘ └──────────────────┘| 阶段 | Embedded | Standard | Full |
|---|---|---|---|
| 编译 | 相同 | 相同 | 相同 |
| 执行模式 | 同步 | spawn 块内并发 | 并行 |
| 内存占用 | 低 | 中 | 高 |
| 并发能力 | 无 | spawn 块内 | spawn 块内 + 并行 |
| spawn 支持 | ❌ | ✅ | ✅ |
| DAG 分析 | 无 | spawn 块内 | spawn 块内 |
| WorkStealer | 无 | 无 | ✅ |
Embedded Runtime:目标 WASM/游戏脚本/规则引擎。即时执行器,无 spawn 支持,高性能低占用。
Standard Runtime:目标 Web 服务/数据管道。支持 spawn {} 块,在 spawn 块内进行 DAG 分析和自动并发。num_workers=1 即单线程异步。
Full Runtime:目标科学计算/大规模并行。Standard + WorkStealer 负载均衡。
2. 调度器脱耦:泛型 + 注入
核心原则:VM 不直接依赖具体调度器,通过泛型参数 [S] 调用。
# 调度器接口定义
Scheduler: Type = {
spawn: (Task) -> TaskId,
await: (TaskId) -> Result,
spawn_with_deps: (Task, List(TaskId)) -> TaskId,
await_all: (List(TaskId)) -> List(Result),
stats: () -> SchedulerStats,
}
# 单线程调度器
SingleThreadScheduler: Scheduler = {
spawn: (task) => { task_queue.push(task); generate_task_id() },
await: (task_id) => { ... },
spawn_with_deps: (task, deps) => { ... },
await_all: (task_ids) => { ... },
stats: () => { queue_size: task_queue.len() },
}
# 多线程调度器
MultiThreadScheduler: Scheduler = {
spawn: (task) => { work_queue.push(task); generate_task_id() },
await: (task_id) => { wait_for_completion(task_id) },
spawn_with_deps: (task, deps) => { ... },
await_all: (task_ids) => { ... },
stats: () => { workers: get_worker_stats() },
}
# VM 通过泛型使用调度器
create_vm: [S: Scheduler](scheduler: S) -> VM = (scheduler) => {
VM(scheduler: scheduler, memory: create_memory(), dag: create_dag())
}核心要点:
- 编译期多态,零运行时开销
- 无需 Trait 对象
- 泛型类型约束
[S: Scheduler]已在 RFC-011 中定义
3. 同步 = 调度的特例
❌ 误解:禁用调度器
✅ 正确:使用单 worker 的调度器
num_workers = 1 → 单线程异步调度
num_workers > 1 → 多线程并行调度
同一个调度器接口,只是配置不同。消除特殊情况。4. DAG 的地位
重要变更:DAG 分析不再作用于全程序,仅在
spawn {}块内部进行。普通代码(spawn 块外部)是顺序执行的,无需 DAG 分析。
| 层级 | spawn 支持 | DAG 分析范围 | 说明 |
|---|---|---|---|
| Core Runtime | ✅ | spawn 块内 | 并发核心 |
| Standard Runtime | ✅ | spawn 块内 | spawn + DAG 调度 |
| Embedded Runtime | ❌ | 无 | 即时执行,无并发 |
5. 自底向上执行模型(spawn 块内)
重要变更:自底向上 DAG 分析仅在
spawn {}块内部进行,不再对全程序进行 DAG 分析。
用户代码(spawn 块内并发):
(a, b) = spawn {
fetch(url0),
fetch(url1)
}
print(a)
编译时分析(spawn 块内自底向上):
fetch(url0) 和 fetch(url1) 无相互依赖 → 可并行执行
spawn 块外的 print(a) → 顺序执行,等待 spawn 完成
运行时调度(spawn 块内从叶子开始):
fetch(url0) ┐
├→ 并行执行
fetch(url1) ┘
print(a) ← spawn 块外,顺序执行核心要点:
- 自底向上依赖分析仅限于
spawn {}块内部 - spawn 块内无依赖的任务并行执行
- spawn 块外的代码按顺序执行,等待 spawn 块完成
6. 编译模型:双后端 + 静态链接运行时
6.1 两个后端,一种行为
┌─────────────────────┐
│ 编译前端(统一) │
│ Lexer → Parser │
│ → TypeCheck │
│ → spawn 块内 DAG 分析│
│ → 逃逸分析 │
│ → 环检测 │
└──────────┬──────────┘
│
┌────────────┴────────────┐
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ VM 后端(开发) │ │ LLVM 后端(生产) │
│ │ │ │
│ 生成 IR/字节码 │ │ 生成原生代码 │
│ VM 解释执行 │ │ 链接运行时静态库 │
│ 支持步进调试 │ │ 输出 .exe │
│ 快速迭代 │ │ 零解释开销 │
└───────────────────┘ └───────────────────┘
│ │
▼ ▼
行为完全一致 行为完全一致VM 后端:开发时使用。修改代码 → 即时运行 → 步进调试 → 快速迭代。行为和最终 exe 完全一致。
LLVM 后端:发布时使用。AOT 编译到原生代码,调度器作为静态库链接进去。没有解释器,没有 JIT。
6.2 调度器 = 静态库,不是虚拟机
最终 exe 的内部结构:
┌────────────────────────────────────────────┐
│ 你的代码(原生机器码) │
│ ├── 编译期已确定的 DAG 执行计划 │
│ ├── 内联的 Move/ref/clone 操作 │
│ └── RAII 释放代码 │
├────────────────────────────────────────────┤
│ 运行时静态库(~200-500KB) │
│ ├── 线程池(固定大小 = num_workers) │
│ ├── 事件循环(libuv / io_uring) │
│ ├── 工作窃取队列(仅 Full Runtime) │
│ ├── 内存分配器(jemalloc / mimalloc) │
│ └── 反射元数据(按需加载,不常驻内存) │
├────────────────────────────────────────────┤
│ 没有: │
│ ❌ 字节码解释器 │
│ ❌ JIT 编译器 │
│ ❌ GC │
│ ❌ 虚拟机 │
└────────────────────────────────────────────┘对比:
| 语言 | Java | Go | YaoXiang |
|---|---|---|---|
| 编译产物 | 字节码 | 原生代码 | 原生代码 |
| 执行方式 | JVM 解释/JIT | 直接执行 | 直接执行 |
| 运行时大小 | ~200MB(JVM) | ~1-2MB(含 GC) | ~200-500KB(无 GC) |
| 内存管理 | GC | GC | RAII(确定) |
| 反射 | 常驻内存 | 常驻内存 | exe 中储存,按需加载 |
6.3 为什么调度器性能恒定
关键洞察:大部分工作在编译期完成,运行时只做"执行"。
编译期(一次性,不进入运行时):
├── 分析 spawn 块内 DAG:谁依赖谁
├── 拓扑排序:确定 spawn 块内执行顺序
├── 识别可并行任务:spawn 块内无依赖的子树
├── 逃逸分析:ref → Rc 还是 Arc
├── 环检测:自动降级 Weak 或报错
└── 内联:小函数直接展开
运行时(每次执行,数据结构固定):
├── 按编译期确定的 spawn 块 DAG 顺序分发任务到线程池
├── 遇到 I/O → 挂起当前任务,事件循环接管
├── 任务就绪 → 放回就绪队列
└── 就这些。调度器本身是固定大小的数据结构:线程池、事件循环、工作队列。没有动态增长,没有自适应重优化,没有 GC 扫描。行为完全可预测。
编译期已经把 spawn 块内"调度什么"算完了,运行时只做"执行"。这跟 tokio 不同——tokio 在运行时动态构建 Future 链。YaoXiang 的 DAG 是静态的,且仅限于 spawn 块内。
6.4 反射:储存不常驻
反射元数据在编译期生成,储存在 exe 的独立段(section)中。程序启动时不加载。首次请求反射时,按需 mmap 进内存。类似于:
exe 布局:
.text ← 你的代码
.rodata ← 常量
.reflect ← 反射元数据(类型信息、函数签名等)
mmap 按需加载,不访问不占内存取舍:exe 体积增大(含反射数据),但运行时不访问则零内存开销。首次访问有加载延迟(类似 JIT 预热),后续零开销。
src/
├── lib.rs
├── main.rs
├── backends/ # 运行时后端
│ ├── common/ # 所有后端共享(值、堆、操作码)
│ │ ├── allocator.rs
│ │ ├── heap.rs
│ │ ├── opcode.rs
│ │ └── value.rs
│ ├── dev/ # REPL + 调试器
│ │ ├── debugger.rs
│ │ ├── shell.rs
│ │ └── repl/
│ ├── interpreter/ # 🟢 树遍历解释器(原 Embedded/VM)
│ │ ├── ffi.rs
│ │ ├── frames.rs
│ │ ├── registers.rs
│ │ ├── runtime.rs
│ │ └── executor/
│ └── runtime/ # 🔵 编译型 VM 运行时
│ ├── engine.rs
│ ├── facade.rs
│ └── task.rs
├── frontend/ # 编译前端(所有后端共享)
│ ├── compiler.rs
│ ├── config.rs
│ ├── pipeline.rs
│ ├── core/
│ │ ├── lexer/
│ │ ├── parser/
│ │ ├── typecheck/
│ │ │ ├── checker.rs
│ │ │ ├── spawn_placement.rs # ★ spawn 块内 DAG/并发分析(原 frontend/dag/)
│ │ │ ├── inference/
│ │ │ └── traits/
│ │ └── types/
│ ├── events/
│ ├── module/
│ └── pipeline/
├── middle/ # 中间端
│ ├── core/ # IR & 字节码
│ │ ├── bytecode.rs
│ │ ├── ir.rs # IR 定义(VM 和 LLVM 共用)
│ │ └── ir_gen.rs
│ └── passes/ # 编译 pass
│ ├── codegen/ # 代码生成(原 codegen/)
│ ├── lifetime/ # 生命周期/借用分析
│ └── mono/ # 单态化
├── lsp/ # Language Server
├── formatter/ # 源码格式化器
├── package/ # 包管理器
├── std/ # 标准库
│ ├── concurrent.rs
│ ├── io.rs
│ ├── list.rs
│ ├── math.rs
│ ├── net.rs
│ ├── string.rs
│ └── weak.rs
└── util/ # 工具库
├── diagnostic/
├── i18n/
└── config/目录映射说明(旧 → 新):
| 旧目录 | 新位置 | 说明 |
|---|---|---|
frontend/dag/ | frontend/core/typecheck/spawn_placement.rs | spawn 块内 DAG 分析已整合进类型检查 |
codegen/ | middle/passes/codegen/ | 代码生成移入中间端 pass |
embedded/ | backends/interpreter/ | 树遍历解释器 |
runtime/ | backends/runtime/ | 编译型 VM 运行时 |
vm/ | backends/interpreter/ | 与 embedded 合并 |
full/ | (未实现) | Full Runtime + 工作窃取,待后续版本 |
reflect/ | (未实现) | 反射元数据,待后续版本 |
core/ | backends/common/ | 共享值/堆/操作码 |
权衡
优点
- 清晰分层:Embedded / Standard / Full 三层
- 编译复用:前端代码完全共享
- 泛型脱耦:编译期多态,零开销
- 一致性:同步只是调度的特例
- 嵌入式友好:高性能 + 低内存 + 快速启动
缺点
- 初始复杂度:需要定义调度器接口和多种运行时变体
- 编译期绑定:调度器类型在编译期确定
设计决策记录
| 决策 | 决定 | 日期 |
|---|---|---|
| 调度器脱耦方案 | 泛型 + 注入 | 2025-01-05 |
| 单线程模式 | 同步是调度的特例 | 2025-01-05 |
| 异步实现 | DAG 天然支持 | 2025-01-05 |
| WorkStealer | Full Runtime 高级特性 | 2025-01-05 |
| 嵌入式设计 | 即时执行,无 DAG 调度 | 2025-01-05 |
| 编译阶段 | 所有运行时共享同一套前端 | 2025-01-05 |
| 运行时分层 | Embedded / Standard / Full | 2025-01-05 |
| 类型约束 | RFC-011 已定义 | 2025-01-25 |
| 依赖图构建 | 静态依赖图,编译期确定 | 2025-01-05 |
| 双后端模型 | VM(开发调试)+ LLVM AOT(生产),行为一致 | 2026-05-11 |
| 调度器形态 | 静态库链接进 exe,~200-500KB,无 GC | 2026-05-11 |
| 反射元数据 | 编译进 exe 独立段,mmap 按需加载 | 2026-05-11 |
| 调度器性能 | 编译期完成 DAG 分析,运行时仅执行 | 2026-05-11 |
| DAG 范围对齐 | DAG 分析仅限 spawn 块内,对齐 RFC-024 | 2026-06-05 |
| 三层架构更新 | Embedded 无 spawn,Standard 支持 spawn | 2026-06-05 |
参考文献
生命周期与归宿
| 状态 | 位置 | 说明 |
|---|---|---|
| 草案 | docs/design/rfc/ | 作者草稿 |
| 审核中 | docs/design/rfc/ | 开放社区讨论 |
| 已接受 | docs/design/accepted/ | 正式设计文档 |
| 已拒绝 | docs/design/rfc/ | 保留在 RFC 目录 |
