Skip to content

这是一个非常关键的战术决策。作为架构师,我给出明确的建议:

结论:必须尽早引入 Task/Msg 体系。现在的优先级应该是:Task 架构 > 基础指令实现。

理由很简单:并发模型是虚拟机的“骨架”,而普通指令(加减乘除、类、函数)只是“血肉”。 先填满血肉再换骨架,通常意味着重写核心执行引擎。

以下是详细的深度分析,告诉你为什么要现在就做,以及如何低成本地开始:

1. 为什么不能“以后再加”? ​

如果你现在按照单线程/同步模型去实现 AutoVM(AutoVM),你的核心循环(Dispatch Loop)大概长这样:

rust
// 同步模型(现在的样子)
impl VM {
    fn run(&mut self) { // 这是一个同步函数,一旦进入就独占线程
        loop {
            let op = self.fetch();
            match op {
                OP_ADD => self.do_add(),
                OP_CALL_NATIVE => self.call_c(), // 可能会阻塞!
                // ...
            }
        }
    }
}

后果:

  1. 阻塞灾难:如果你想在这里加入 RECV(等待消息)或者 CALL_JS(异步 Promise),整个 Rust 线程会被卡死。在 Tokio 的世界里,阻塞线程是绝对禁止的。
  2. 重构地狱:将来你要支持 Task,就需要把这个 run 函数改成 async fn,并且要把 VM 拆分成 Scheduler 和 TaskContext。这意味着你现在写的所有关于栈管理、Call Frame 管理的代码,到时候都要大改。
  3. FFI 接口不兼容:如果你现在的 FFI 接口设计成同步的(fn call() -> Value),将来切到异步时,所有插件接口都要改成 fn call() -> Future<Output=Value>,生态代码全部作废。

2. “尽早引入”不代表要实现全部 ​

你不需要现在就实现复杂的 Channel 缓冲区、优先级调度或者跨线程通信。你需要做的是确立数据结构和执行流。

你需要立刻完成的 MVP (最小可行性产品) 如下:

A. 核心结构体:从 VM 变成 Task ​

不要只有一个全局的 VM。现在就定义 Task:

rust
struct Task {
    id: TaskId,
    stack: Vec<Value>, // 每个 Task 都有独立的虚拟栈
    frames: Vec<CallFrame>,
    ip: usize,
    status: TaskStatus, // Running, Waiting, Ready
}

struct AutoVM {
    // 调度器持有一堆 Task
    tasks: HashMap<TaskId, Arc<Mutex<Task>>>, 
    // Tokio 的 Handle,用于真正的调度
    runtime: tokio::runtime::Runtime,
}

B. 执行循环:从 loop 变成 Future ​

这是最关键的一步。把指令执行逻辑包装成一个 Rust Future(或者 async 块)。

rust
impl AutoVM {
    // 启动一个 Task
    fn spawn(&self, task_id: TaskId) {
        let task = self.tasks.get(&task_id).unwrap().clone();
        
        // 扔给 Tokio 去跑
        tokio::spawn(async move {
            loop {
                // 1. 获取锁,执行 N 条指令 (协作式)
                let mut guard = task.lock().await;
                let should_yield = guard.run_steps(100); 
                drop(guard); // 释放锁,让其他 Tokio 任务有机会运行

                // 2. 如果遇到 RECV 或时间片用完,主动让出
                if should_yield {
                    tokio::task::yield_now().await;
                }
            }
        });
    }
}

3. 执行策略建议 ​

不要停下来等待 Task 体系完美才继续。 采用**“架构先行,特性填充”**的策略:

  1. Week 1 (当前):
  • 重构: 引入 tokio 依赖。
  • 定义: 把 VM 里的栈和 IP 挪到 Task 结构体里。
  • 跑通: 写一个 OP_SPAWN 指令,能启动两个简单的死循环计数任务,在控制台交替打印数字。
  • 里程碑:证明 M:N 调度模型已在 Rust 层跑通。
  1. Week 2 (后续):
  • 填充: 在这个新的 Task 架构下,去实现 OP_ADD, OP_CALL 等常规指令。
  • 注意:因为架构对了,这时候填入的每个指令未来都不需要改。
  1. Week 3 (异步):
  • 扩展: 实现 OP_RECV。这时候你只需要在 run_steps 里返回一个 State::Waiting,Tokio 这一层自然就处理好了。

4. 关于 MicroVM 的思考 ​

你可能会担心:“AutoVM 用了 Tokio,那 MicroVM 怎么办?MCU 上没有 Tokio 啊。”

不用担心。 正如我们之前讨论的,两者的 数据模型 (Data Model) 是一致的:

  • 都有 Task 结构体。
  • 都有 Stack。
  • 都有 IP。

区别只在于 调度循环 (The Loop):

  • AutoVM: 用 tokio::spawn 驱动 task.run_step()。
  • MicroVM: 用 xTaskCreate (FreeRTOS) 驱动 task.run_step()。

task.run_step() 里面的核心解释逻辑(字节码 switch-case)是可以复用的! 甚至可以把核心解释逻辑抽离成一个 auto_core crate(no_std),让 AutoVM 和 MicroVM 共享。

总结 ​

长痛不如短痛。

现在引入 Task/Msg 体系的成本是最低的(可能只需要改几百行代码框架)。 如果你等基本功能都写完了再改,那就是伤筋动骨(几千行代码的逻辑重写),而且很容易引入极其难调的并发 Bug。

行动指令: 请立刻让负责生成 AutoVM 的 AI 暂停填充具体指令,先生成基于 Tokio 的 Task 调度框架。框架定好了,再填指令就是填空题了。

基于 MIT 许可发布。