Skip to content
📖 Auto 编程语言(主书)本书目录

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!

让我们分析一下发生了什么:

  1. fn greeter() —— 定义了一个将作为 Actor 运行的函数。它进入一个 无限循环(for true)等待消息。

  2. on msg String —— 这是 Auto 的消息接收语法。它等待一个 String 类型的消息,并将其绑定到变量 msg。当消息到达时,on 块的主体执行。

  3. spawn greeter —— 将 greeter 函数作为独立的 Actor 启动运行。 主程序继续执行,不会等待。

  4. send greeter "Hello from main!" —— 向 greeter Actor 发送一条 字符串消息。send 关键字是即发即忘的:它传递消息并立即返回。

与其他语言的对比非常鲜明。在 Rust 中,你需要用 mpsc::channel() 创建 通道,将接收端传入生成的线程,并在每次发送时调用 .unwrap()。在 Python 中,你需要 threading.Thread 和 queue.Queue。在 C 中,根本没有标准的 消息传递原语。在 Auto 中,只需两行代码:spawn 和 send。

发送消息 ​

在前面的例子中,greeter Actor 只能理解 String 消息。Actor 可以接收 任意类型的消息。一种常见模式是使用 enum 定义 Actor 接受的命令集。

使用类型化命令消息的 Actor

这里的关键要点:

  1. enum Command —— 定义了 Actor 接受的消息协议。枚举非常适合这种 用法,因为它们穷举列出了所有可能的命令,编译器确保你处理了所有情况。

  2. on cmd Command —— 接收一个 Command 消息。主体使用 is 模式 匹配语法(来自[第 7 章][ch7])对每个变体进行分发。

  3. send caller count —— 特殊标识符 caller 指向发送当前消息的 Actor。 这是 Actor 向发送者回送结果的方式。

  4. send counter Command.Increment —— 消息是完全类型化的。编译器确保 你只能向 counter Actor 发送 Command 值。

在 Rust 中,这需要枚举定义、match 表达式、通道创建和线程生成。在 Python 中,你需要单独的 Enum 类和显式的 if/elif 链。Auto 将所有这些 折叠为带有模式匹配的 on 表达式。

使用 on 接收消息 ​

on 关键字是 Actor 接收消息的方式。它会阻塞,直到有消息到达,然后将消息 绑定到变量并运行处理程序主体。处理程序可以使用我们已学过的任何模式匹配 语法。

使用 `on` 接收类型化消息

输出如下:

Fetched: https://auto.dev

Request 类型是一个自定义类型(来自第 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 一次 处理一条消息,因此状态变更总是顺序的。

具有隔离状态的 Actor

输出如下:

counter: 1
counter: 2
Unknown command: hello

stateful_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 接收消息。

`main` 作为接收消息的主 Actor

输出如下:

Total so far: 20

这个例子展示了一个完整的请求-响应周期:

  1. main 生成一个 worker 并向其发送数字 10。
  2. worker 在 on n int 中接收数字,计算 n * 2,并使用 send main 将结果发回给 main。
  3. main 在 on result int 中接收结果并更新 total。

Worker 函数中的 send main 语法指向主 Actor。这就是 Worker 向协调器报告 的方式。在 Rust 中,这需要创建两个通道(每个方向一个)并将发送端传入生成 的线程。在 Auto 中,这只是一个表达式。

这个模式——main 作为协调器,生成 Worker 并收集结果——是 Auto 中最常见的 并发模式之一。它是扇出、MapReduce 和管道架构的基础。

多个 Actor ​

实际程序很少只有一个后台任务。当你有多个 Actor 并行工作,各自处理问题的 不同部分时,Actor 模型才真正发挥其威力。

生成带参数的多个 Worker

输出如下:

Worker 1 computing 42
Worker 2 computing 100
Worker 3 computing 7

关键观察:

  1. fn worker(id int) —— Worker 函数接受一个参数。当你 spawn worker(1) 时,Auto 创建一个新 Actor,id 设为 1。

  2. spawn worker(1)、spawn worker(2)、spawn worker(3) —— 三个独立 的 Actor,各有自己的 id 和消息队列。

  3. send worker(1) Job.Compute(42) —— 消息被发送到特定的 Actor 实例。 worker(1) 和 worker(2) 是不同的 Actor,即使它们运行相同的函数。

  4. 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 消息类型自然配合。

跨 Actor 边界传播错误

输出如下:

Processed: hello world
Error: Empty data

processor Actor 验证其输入。如果字符串非空,它向 main 发送 WorkResult.Success。如果字符串为空,它发送 WorkResult.Error。main Actor 接收结果并使用 is 模式匹配处理两种情况。

这个模式——将结果包装在成功/错误枚举中并作为消息发送——是 第 9 章!T(Result)类型的 Actor 模型等价物。错误就是数据。它们通过与其他所有 内容相同的消息系统流动。没有异常,没有 panic/unwind,没有 try/catch。如果 Actor 遇到问题,它会发送一条错误消息。调用者决定如何处理。

总结 ​

本章介绍了 Actor 模型——Auto 内置的并发方法。以下是我们学到的内容:

概念语法用途
生成 Actorspawn name将函数作为独立任务启动
发送消息send actor value向 Actor 传递数据(即发即忘)
接收消息on var Type { }阻塞等待消息到达
回复发送者send caller value将结果发送回消息发送者
发送给 mainsend main value回复主 Actor
带参数生成spawn fn(args)从一个函数创建多个 Actor 实例
寻址实例send fn(id) value向特定 Actor 实例发送消息

关键要点:

  1. Actor 就是函数 —— 任何函数都可以通过 spawn 成为 Actor。Actor 在 循环中运行函数体,一次处理一条消息。

  2. 通信通过消息 —— Actor 永远不共享内存。它们只通过 send 和 on 通信。

  3. 状态是隔离的 —— 在 Actor 函数内声明的变量是该 Actor 私有的。不需要 锁,因为一次只处理一条消息。

  4. main 是一个 Actor —— 主 Actor 可以使用 on 接收消息,使其成为 Worker Actor 的自然协调器。

  5. 错误就是消息 —— 跨 Actor 的错误传播使用与普通数据相同的消息系统。 不需要异常。

  6. 该模型消除了 bug 类别 —— 无数据竞争(无共享内存)、无死锁(无锁)、 无过期引用(所有权转移)。

Actor 模型是 Auto 能够作为 AI 操作系统的原因。每个后台任务、每个服务、 每个 AI 代理都是一个 Actor。它们通过消息通信,独立失败,自然跨核心扩展。 在下一章中,我们将以此为基础,探索更高级的并发模式。

基于 MIT 许可发布。