异步与 ~T
在第 15 章中,我们学习了 Auto 的 Actor 模型——如何生成任务、发送消息、 使用 on 接收。Actor 通过消息传递为我们提供了并发能力。但 Auto 中还有第二个同 样重要的并发模型:使用 ~T 类型修饰符的_异步_编程。
在本章中,我们将学习 Auto 的 ~T(读作"波浪-T")如何提供一种干净、可组合 的方式来表达异步操作——无需 async/await 关键字的繁琐或外部运行时。
Auto 中的异步
大多数编程语言使用两个关键字来处理异步操作:async 标记函数,await 等待 结果。JavaScript 有 async/await。Python 有 async/await。C# 有 async/await。 Rust 有 async fn 和 .await。
Auto 采用了不同的方法。它没有添加关键字,而是使用了一个_类型修饰符_:波浪号 ~。当你看到 ~String 时,它表示"一个将来会到达的 String"。当你看到 ~int 时,它表示"一个将来会到达的 int"。~ 是一个_时间蓝图_——对某个计算将在未来 某个时刻完成的描述。
为什么不用 async/await?三个原因:
关键字太重 ——
async/await需要在语言中引入两个新关键字。~T不需要 任何新关键字。波浪号是一个类型修饰符,就像?T表示可选、!T表示结果 一样。组合更自然 ——
~T可以与其他类型修饰符组合。你可以写~?String(一个 未来的可选字符串)或~!int(一个未来的结果整数)。而使用async/await, 这些组合要么笨拙,要么不可能。Actor 和异步统一 —— 在 Auto 中,
on已经可以处理等待消息。~T类型 让异步函数自然地参与 Actor 系统,不需要单独的运行时或不同的规则集。
这与 Rust 伴读书第 17 章中深入探讨的 ~T 概念相同。在 TAPL 中,我们重点关注 ~T 如何在 Auto 基于 Actor 的并发模型中工作。
~T 蓝图
返回 ~T 的函数就是异步函数。它不会立即返回值——它返回一个值的_蓝图_,该 值将在稍后产生。调用者可以决定何时等待它。
让我们分析一下关键要点:
fn fetch_data() ~String—— 返回类型是~String,不是String。这告诉 编译器(和读者)该函数返回一个时间蓝图。实际的字符串将在稍后产生。on result String—— 在异步函数内部,on等待一个触发器。当函数被调用 时,调用者立即获得一个蓝图。函数体异步运行,当它到达on时,它等待将 产生结果的触发条件。send main result—— 当结果准备好时,将其发送回调用者(在这里是main)。这完成了蓝图。spawn fetcher——fetch_data函数被生成为一个 Actor。因为它返回~String,生成操作创建了一个异步 Actor,将在某个时刻产生一个字符串。on data String—— 调用者使用on接收完成的值。这与我们在第 15 章中学到的on关键字相同,但在这里它接收的是异步操作的结果。
关键洞察是 ~T 和 Actor 模型使用相同的原语:on 等待,send 传递。没有 单独的异步运行时。异步函数_就是_碰巧返回一个值的 Actor。
在 TypeScript 中,等价物是 Promise<string> —— 一个解析为字符串的 Promise。 在 Rust 中,等价物是 impl Future<Output = String>。Auto 的 ~String 更简短、 更清晰,并且可以与类型修饰符系统组合。
异步函数
现在让我们看一个更完整的异步函数——一个接受参数并产生结果的函数。
输出如下:
Hello, Alice!有几个注意点:
fn delayed_greeting(name String) ~String—— 函数接受一个String参数, 返回~String。~String返回类型告诉 Auto 这是一个异步函数。它将产生一个 字符串,但不是现在。on _—— 下划线_表示"等待任何触发器"。在真正的异步函数中,这会是 网络响应、定时器或来自另一个 Actor 的消息。这里模拟等待异步工作完成。send caller—— 特殊标识符caller指向调用此异步函数的地方。这与 第 15 章中 Actor 的caller相同,但在异步上下文中它表示"持有蓝图的 地方"。var result = delayed_greeting("Alice")—— 调用立即返回一个蓝图。变量result持有一个~String,而不是String。实际的问候还没有产生。on msg String—— 主 Actor 等待蓝图完成。当问候准备好时,msg接收 实际的String值。
流程是:调用返回蓝图 -> 异步函数在后台运行 -> 结果被发送回来 -> 调用者用 on 接收。这与 Actor 消息传递是相同的模式,但类型系统追踪了异步契约。
在 Rust 中,这需要 async fn 和 .await。在 Python 中,async def 和 await。在 TypeScript 中,Promise 和 .then()。在 Auto 中,只需 ~T 和 on。
on 用于异步处理
on 关键字在 Auto 中身兼两职。在第 15 章中,我们用它接收 Actor 消息。 在异步函数中,我们用它等待结果。这不是巧合——它是同一个机制。异步函数_就是_ Actor。
输出如下:
Status: 200这个例子展示了 Actor 和异步的自然融合:
fn http_server()—— 一个带有消息循环的普通 Actor 函数(for true配 合on)。它没有被声明为异步——它是一个碰巧处理请求并发送响应的 Actor。type Request和type Response—— 自定义类型(来自第 6 章), 定义了调用者和服务器之间的协议。服务器接收Request消息并发送回Response消息。send http_server Request { url: "/api/data" }—— 调用者向服务器 Actor 发送请求。这是即发即忘的,就像任何 Actor 消息一样。on resp Response—— 调用者等待Response到达。这是异步部分:调用者 挂起,直到服务器发送回响应。
http_server 函数是一个长期运行的 Actor。它在 ~T 意义上本身不是异步的—— 它不返回蓝图。相反,它是一个处理请求并发送响应的服务。调用者使用异步模式与 它交互:send 发起请求,on 等待响应。
在 Rust 中,这个模式需要显式通道、循环和 recv()。在 Python 中,需要 threading.Thread 和 Queue。在 C 中,没有标准的等价物。在 Auto 中,它是 我们用于所有 Actor 通信的相同的 on/send 模式。
组合异步
~T 的真正威力在你需要链接多个异步操作时显现。当一个异步操作依赖于另一个 操作的结果时,你嵌套 on 块。每个 on 等待其蓝图完成后才运行内部代码。
输出如下:
User: User1
Posts: Posts for User1这是异步链接模式:
var user = fetch_user(1)—— 调用第一个异步函数。user是一个~String蓝图。函数开始在后台运行。on u String { ... }—— 等待第一个蓝图完成。完成后,u持有实际的String值("User1")。var posts = fetch_posts(u)—— 使用第一个操作的结果作为第二个操作的输入。 这只在第一个on完成后才运行,因此u保证是一个真实值。on p String { ... }—— 等待第二个蓝图完成。在这个块内,u和p都 作为真实值可用。
on 块的嵌套是 Auto 中等价于 JavaScript 的 .then() 链或 Rust 中顺序 .await 的方式。每个 on 引入一个作用域,在其中蓝图已被解析,值可用于使用。
在 TypeScript 中,这将是 await fetchUser(1) 后跟 await fetchPosts(u)。 在 Rust 中,将是 let u = fetch_user(1).await 后跟 let p = fetch_posts(&u).await。 在 Auto 中,on 块实现了相同的效果,同时保持在 Actor 消息传递范式内。
异步 + Actor
Auto 异步模型最深刻的洞察是异步和 Actor 是同一件事。异步函数就是返回值的 Actor。Actor 就是并发运行的函数。~T 类型修饰符是连接它们的桥梁:它标记一个 将为其调用者产生结果的函数。
这意味着 Actor 可以调用异步函数,异步函数可以向 Actor 发送消息,一切自然 组合。
输出类似如下:
Processed: task-a
Processed: task-b(顺序可能不同,因为 Worker 并发运行。)
这个例子把我们学到的所有内容整合在一起:
enum Work—— 定义消息协议。Process(String)是执行工作的命令;Result(String)是完成的结果。这与第 15 章的 Actor 通信模式和 第 9 章的错误处理模式相同。fn worker(id int)—— 一个带参数的 Actor(来自第 15 章)。每个 实例有自己的id和邮箱。Worker 作为独立的 Actor 运行。is work { Process(data) => ..., Result(_) => ... }—— 对Work枚举进行 模式匹配(来自第 7 章)。Worker 处理Process消息,忽略Result消息。spawn worker(1)、spawn worker(2)—— 两个独立的 Worker Actor。每个从 自己的邮箱处理消息。send worker(1) Work.Process("task-a")—— 将工作分发到特定 Worker。Work枚举将数据与类型信息一起包装。on result Work { is result { Result(msg) => print(msg), ... } }—— 主 Actor 接收 Worker 的结果并进行模式匹配以提取消息。
Work 枚举是这里的关键设计模式。通过将命令和结果包装在同一个枚举类型中, Actor 系统提供了类型安全的通信。编译器确保每个变体都被处理,防止因未处理 消息类型而导致的 bug。
这就是使 Auto 作为 AI 操作系统强大的 Actor-异步模式。Worker 是 Actor。任务 是消息。结果是消息。类型系统确保一切协调一致。没有共享内存,没有锁,没有 竞争条件。
超时与取消
当你生成一个异步操作时,你可能不想永远等待。如果网络很慢怎么办?如果计算 耗时太长怎么办?你需要一种方法来设置时间限制,并在操作未按时完成时取消它。
输出如下:
Done!这个例子展示了超时和取消的基本结构。long_running 函数模拟一个耗时的操作。 main 函数启动它并用 on 等待结果。
在实际实现中,你会添加一个超时机制。模式将是:
- 启动异步操作(
var job = long_running()) - 同时启动一个定时器
- 使用
on接收先完成的结果——实际结果或超时
因为 Auto 的 Actor 模型是基于消息的,取消也是基于消息的。你会向运行长操作 的 Actor 发送一条取消消息,该 Actor 在其消息循环中检查该消息。这从根本上比 其他语言中的线程取消更简单,在其他语言中你需要处理中断标志、 CancellationToken 或 thread.interrupt()。
关键洞察是在 Auto 中,一切都是 Actor,Actor 通过消息通信。这意味着超时和 取消不是特殊的运行时功能——它们只是另一种消息模式。你不需要特殊 API。你不 需要 CancellationToken 类型。你发送一条"停止"的消息,Actor 就停止了。
在 Rust 中,等价的模式使用 tokio::select! 或 tokio::time::timeout。在 Python 中,asyncio.wait_for。在 JavaScript 中,Promise.race。在 Auto 中, 相同的 Actor 原语处理正常完成和超时。
总结
本章介绍了 ~T —— Auto 的异步编程方法。以下是我们学到的内容:
| 概念 | 语法 | 用途 |
|---|---|---|
| 异步返回类型 | fn foo() ~T | 声明一个返回未来值的函数 |
| 等待结果 | on var T { } | 阻塞直到蓝图解析 |
| 回复调用者 | send caller value | 用值完成蓝图 |
| 链接异步操作 | 嵌套 on 块 | 运行顺序异步操作 |
| 与 Actor 组合 | spawn + send + on | 混合异步函数和 Actor 模式 |
| 超时/取消 | 基于消息 | 将取消作为普通消息发送 |
关键要点:
~T是类型修饰符,不是关键字 —— 它使用与?T(可选)和!T(结果) 相同的系统。不需要新关键字。异步和 Actor 是同一件事 —— 异步函数就是返回值的 Actor。
on/send原语对两者都适用。通过嵌套组合 —— 通过嵌套
on块链接异步操作。每个on解析一个蓝图 并使值可用。全程类型安全 —— 编译器追踪哪些值是蓝图(
~T),哪些已解析(T)。 你不能意外地将蓝图用作真实值。超时只是一种消息 —— 因为一切都是基于 Actor 的,超时和取消是消息模式, 不是特殊的运行时功能。
~T 类型修饰符是 Auto 并发故事的最后一块拼图。结合第 15 章的 Actor 模型,它提供了一个完整的系统来编写并发程序:Actor 用于有状态的长期运行服务, ~T 用于产生结果的请求/响应模式。它们共同使 Auto 天然适合 AI 操作系统模型, 其中数十个服务并发运行,通过消息通信,并组合它们的结果。
在下一章中,我们将探索 Auto 如何使用 is、has 和 spec 关键字处理面向 对象编程。