Actor 并发
本章开启 TAPL 第三阶段——Auto 作为 AI 操作系统展现其全部实力的阶段。 Actor 模型是 Auto 的基础。它不是一个附加库,也不是一个可选特性。它从 语言底层就内建其中。
在本章中,我们将学习如何使用 Auto 内置的 Actor 系统编写并发程序:生成 任务、发送消息、使用 on 接收、管理隔离状态,以及跨 Actor 边界处理错误。
Auto 中的并发
编写同时执行多个操作的程序是软件开发中最困难的问题之一。传统语言使用 线程——运行进程的轻量副本,它们共享内存。线程很强大,但也很危险:两个 线程同时访问同一数据会导致数据竞争、死锁,以及仅在负载高时才出现的微妙 bug。
Auto 采用了根本不同的方法:Actor 模型。在 Actor 模型中,并发通过独立的 actor 表达,它们只通过发送消息进行通信。任何 Actor 都不能直接访问另一个 Actor 的状态。这消除了整类并发 bug:
- 无数据竞争 —— Actor 不共享内存
- 无死锁 —— 没有可以死锁的锁
- 无过期引用 —— 通信通过消息传递,而非共享指针
这个模型自然地映射到 AI 操作系统的工作方式。在 Auto 中,每个任务、每个 服务、每个后台作业都是一个 Actor。它们独立运行,通过消息通信,失败时不会 导致整个系统崩溃。
Auto 的所有权系统(我们在第 12 章学到的)在这里仍然扮演着关键 角色:它确保 Actor 之间发送的消息干净地转移所有权,防止发送者意外使用已经 发送出去的数据。
注意: Auto 也支持传统的基于线程的并发,以便与 Rust 库互操作。本章 重点关注 Actor 模型,这是 Auto 惯用的方法。
生成任务
Auto 中最基本的并发单元是_任务_——一个独立运行的函数。你使用 spawn 关键字创建任务,它会在后台启动函数运行。
输出如下:
Greeting: Hello from main!
Greeting: Auto is awesome!让我们分析一下发生了什么:
fn greeter()—— 定义了一个将作为 Actor 运行的函数。它进入一个 无限循环(for true)等待消息。on msg String—— 这是 Auto 的消息接收语法。它等待一个String类型的消息,并将其绑定到变量msg。当消息到达时,on块的主体执行。spawn greeter—— 将greeter函数作为独立的 Actor 启动运行。 主程序继续执行,不会等待。send greeter "Hello from main!"—— 向greeterActor 发送一条 字符串消息。send关键字是即发即忘的:它传递消息并立即返回。
与其他语言的对比非常鲜明。在 Rust 中,你需要用 mpsc::channel() 创建 通道,将接收端传入生成的线程,并在每次发送时调用 .unwrap()。在 Python 中,你需要 threading.Thread 和 queue.Queue。在 C 中,根本没有标准的 消息传递原语。在 Auto 中,只需两行代码:spawn 和 send。
发送消息
在前面的例子中,greeter Actor 只能理解 String 消息。Actor 可以接收 任意类型的消息。一种常见模式是使用 enum 定义 Actor 接受的命令集。
这里的关键要点:
enum Command—— 定义了 Actor 接受的消息协议。枚举非常适合这种 用法,因为它们穷举列出了所有可能的命令,编译器确保你处理了所有情况。on cmd Command—— 接收一个Command消息。主体使用is模式 匹配语法(来自[第 7 章][ch7])对每个变体进行分发。send caller count—— 特殊标识符caller指向发送当前消息的 Actor。 这是 Actor 向发送者回送结果的方式。send counter Command.Increment—— 消息是完全类型化的。编译器确保 你只能向counterActor 发送Command值。
在 Rust 中,这需要枚举定义、match 表达式、通道创建和线程生成。在 Python 中,你需要单独的 Enum 类和显式的 if/elif 链。Auto 将所有这些 折叠为带有模式匹配的 on 表达式。
使用 on 接收消息
on 关键字是 Actor 接收消息的方式。它会阻塞,直到有消息到达,然后将消息 绑定到变量并运行处理程序主体。处理程序可以使用我们已学过的任何模式匹配 语法。
输出如下:
Fetched: https://auto.devRequest 类型是一个自定义类型(来自第 6 章),有两个字段:url 和 reply_to。fetcher Actor 接收 Request 消息并处理它们。在 main 中, 我们用类型字面量语法构造一个 Request 并发送给 fetcher。
on req Request 语法同时做了两件事:等待一个 Request 类型的消息,并将 接收到的值绑定到变量 req。在处理程序内部,你使用点号访问字段: req.url、req.reply_to。
这个模式——定义消息类型并用 on 接收——是 Auto 中所有 Actor 通信的基础。
Actor 状态
Actor 模型最强大的方面之一是每个 Actor 都可以维护自己的私有状态。因为没有 其他 Actor 能访问这个状态,所以不需要锁、互斥量或任何同步原语。Actor 一次 处理一条消息,因此状态变更总是顺序的。
输出如下:
counter: 1
counter: 2
Unknown command: hellostateful_actor 函数在消息循环之前声明了一个 var state。这个变量在消息 之间持久存在。当 Actor 收到 "inc" 时,它递增 state.count。当收到未知 命令时,它打印错误信息。状态是完全隔离的——只有这个 Actor 能看到或修改它。
在 Rust 中,这个模式需要 Arc<Mutex<State>> 来跨线程边界共享状态。在 Python 中,你依赖 GIL 或显式锁。在 Auto 中,隔离由语言本身保证。不需要锁, 不需要 Arc,不需要 Mutex——只需一个在 Actor 函数内部生存的 var。
这就是我们所说的"Actor 模型内建于语言中"。Auto 不需要单独的并发库,因为 任何函数都可以通过 spawn 成为 Actor,该函数内的每个 var 都自动是 Actor 本地的。
main 作为主 Actor
在 Auto 中,main 本身也是一个 Actor。它是_主 Actor_——程序的入口点和默认 协调器。这意味着 main 也可以像其他 Actor 一样使用 on 接收消息。
输出如下:
Total so far: 20这个例子展示了一个完整的请求-响应周期:
main生成一个worker并向其发送数字10。worker在on n int中接收数字,计算n * 2,并使用send main将结果发回给main。main在on result int中接收结果并更新total。
Worker 函数中的 send main 语法指向主 Actor。这就是 Worker 向协调器报告 的方式。在 Rust 中,这需要创建两个通道(每个方向一个)并将发送端传入生成 的线程。在 Auto 中,这只是一个表达式。
这个模式——main 作为协调器,生成 Worker 并收集结果——是 Auto 中最常见的 并发模式之一。它是扇出、MapReduce 和管道架构的基础。
多个 Actor
实际程序很少只有一个后台任务。当你有多个 Actor 并行工作,各自处理问题的 不同部分时,Actor 模型才真正发挥其威力。
输出如下:
Worker 1 computing 42
Worker 2 computing 100
Worker 3 computing 7关键观察:
fn worker(id int)—— Worker 函数接受一个参数。当你spawn worker(1)时,Auto 创建一个新 Actor,id设为1。spawn worker(1)、spawn worker(2)、spawn worker(3)—— 三个独立 的 Actor,各有自己的id和消息队列。send worker(1) Job.Compute(42)—— 消息被发送到特定的 Actor 实例。worker(1)和worker(2)是不同的 Actor,即使它们运行相同的函数。is job { Compute(n) => ..., Done => ... }——is表达式(来自 第 7 章的模式匹配)在on处理程序内部工作,用于解构枚举变体。Compute(n)提取内部值。
这就是_扇出_模式:将工作分布到多个 Actor。每个 Worker 独立运行,处理自己 的消息,维护自己的状态。如果一个 Worker 崩溃,其他 Worker 不受影响。
在 Rust 中,这需要三对独立的通道。在 Python 中,需要三个独立的 Queue 对象。在 Auto 中,每次 spawn 都会自动创建 Actor 的邮箱——你永远不需要 自己管理通道。
跨 Actor 的错误处理
当 Actor 之间通信时,错误需要跨越 Actor 边界。Actor 模型提供了一种干净的 方式来处理这个问题:将错误信息作为消息发送,就像任何其他数据一样。第 9 章的 !T 错误处理与 Actor 消息类型自然配合。
输出如下:
Processed: hello world
Error: Empty dataprocessor Actor 验证其输入。如果字符串非空,它向 main 发送 WorkResult.Success。如果字符串为空,它发送 WorkResult.Error。main Actor 接收结果并使用 is 模式匹配处理两种情况。
这个模式——将结果包装在成功/错误枚举中并作为消息发送——是 第 9 章!T(Result)类型的 Actor 模型等价物。错误就是数据。它们通过与其他所有 内容相同的消息系统流动。没有异常,没有 panic/unwind,没有 try/catch。如果 Actor 遇到问题,它会发送一条错误消息。调用者决定如何处理。
总结
本章介绍了 Actor 模型——Auto 内置的并发方法。以下是我们学到的内容:
| 概念 | 语法 | 用途 |
|---|---|---|
| 生成 Actor | spawn name | 将函数作为独立任务启动 |
| 发送消息 | send actor value | 向 Actor 传递数据(即发即忘) |
| 接收消息 | on var Type { } | 阻塞等待消息到达 |
| 回复发送者 | send caller value | 将结果发送回消息发送者 |
| 发送给 main | send main value | 回复主 Actor |
| 带参数生成 | spawn fn(args) | 从一个函数创建多个 Actor 实例 |
| 寻址实例 | send fn(id) value | 向特定 Actor 实例发送消息 |
关键要点:
Actor 就是函数 —— 任何函数都可以通过
spawn成为 Actor。Actor 在 循环中运行函数体,一次处理一条消息。通信通过消息 —— Actor 永远不共享内存。它们只通过
send和on通信。状态是隔离的 —— 在 Actor 函数内声明的变量是该 Actor 私有的。不需要 锁,因为一次只处理一条消息。
main是一个 Actor —— 主 Actor 可以使用on接收消息,使其成为 Worker Actor 的自然协调器。错误就是消息 —— 跨 Actor 的错误传播使用与普通数据相同的消息系统。 不需要异常。
该模型消除了 bug 类别 —— 无数据竞争(无共享内存)、无死锁(无锁)、 无过期引用(所有权转移)。
Actor 模型是 Auto 能够作为 AI 操作系统的原因。每个后台任务、每个服务、 每个 AI 代理都是一个 Actor。它们通过消息通信,独立失败,自然跨核心扩展。 在下一章中,我们将以此为基础,探索更高级的并发模式。