Skip to content
📖 Auto 版 Rust Book本书目录

Actor 并发 ​

安全高效地处理并发编程是 Auto 的主要目标之一。_并发编程_是指程序的不同部分独立执行,_并行编程_是指程序的不同部分同时执行。随着越来越多的计算机利用多核处理器,这两者变得越来越重要。

Rust 的并发方法围绕线程、通道和共享状态(Mutex<T> 和 Arc<T>)构建。Auto 采用了不同的方法:Actor 模型。在 Actor 模型中,并发通过独立的_计算单元_(称为_actor_)来表达,它们只通过发送消息来通信。任何 actor 都不能直接访问另一个 actor 的状态。

这个模型消除了整类并发 bug:

  • 无数据竞争 — actor 不共享内存
  • 无死锁 — 没有可以死锁的锁
  • 无过期引用 — 通信通过消息传递,而非共享指针

Auto 的所有权系统仍然扮演着关键角色:它确保 actor 之间发送的消息干净地转移所有权,防止发送者意外使用已发送给另一个 actor 的数据。

注意: Actor 模型是 Auto 的主要并发抽象。为了与 Rust 库兼容,Auto 也通过 use.rust 支持基于线程的并发。本章重点介绍 Actor 模型,这是 Auto 惯用的方法。

本章将涵盖以下主题:

  • Actor 模型与基于线程的并发有何不同
  • 如何创建 actor 并在它们之间发送消息
  • 消息传递模式:请求/响应、扇出和管道
  • 需要与 Rust 线程互操作时的共享状态模式
  • 并发规范:Send 和标记规范

从线程到 Actor ​

在 Rust 中,并发基于_线程_——共享内存的操作系统级执行单元。线程通过_通道_(消息传递)或受互斥锁保护的_共享状态_进行通信。

共享内存的问题 ​

基于线程的共享内存并发虽然强大,但容易出错:

  • 竞争条件:线程以不一致的顺序访问数据
  • 死锁:两个线程互相等待,导致都无法继续
  • 微妙的 bug:只在特定情况下出现,难以复现

Rust 的所有权系统有助于在编译时防止许多这些问题,但心智模型仍然复杂。你必须同时考虑锁、生命周期和线程边界。

Actor 模型 ​

Actor 模型最早由 Carl Hewitt 于 1973 年提出,采用了不同的方法。Actor 是计算的基本单元,具有以下属性:

  1. 每个 actor 有自己的私有状态——其他 actor 无法访问
  2. Actor 只通过发送异步消息进行通信
  3. 当 actor 收到消息时,它可以:
    • 向其他 actor 发送消息
    • 创建新的 actor
    • 为下一条消息改变自己的状态

这个模型自然地映射到许多现实世界的并发问题,并避免了共享内存并发的陷阱。

Actor 模型 vs 线程模型 ​

概念Rust(线程)Auto(Actor)
并发单元线程Actor
通信方式通道或共享状态消息传递
状态共享Mutex + Arc不共享
数据竞争由所有权防止设计上不可能
死锁可能不可能(无锁)
错误处理Result 类型消息级错误

创建 Actor ​

在 Auto 中,使用 actor 关键字定义 actor。一个 actor 封装自己的状态,并定义它可以接收哪些消息。

定义 Actor ​

一个简单的计数器 actor

Counter actor 维护一个私有的 count 字段。on Increment 块处理传入的 Increment 消息。on GetCount -> int 块处理 GetCount 消息并返回当前计数。

与 Rust 方法的主要区别:

  • 无需 Arc<Mutex<T>> — actor 私有拥有其状态
  • 无需 move 闭包 — actor 模型自动处理所有权转移
  • 无需 .lock().unwrap() — 不需要锁,消息逐一处理

Actor 生命周期 ​

每个 actor 经历以下阶段:

  1. 创建 — Counter.init() 创建新的 actor 实例
  2. 运行 — actor 处理邮箱中的消息
  3. 关闭 — 当所有引用被 drop 时,actor 停止

发送消息 ​

有两种向 actor 发送消息的方式:

  • send — 即发即忘(异步,不期望响应)
  • ask — 请求-响应(等待返回值)
auto
counter.send(Increment)         // 即发即忘
let n = counter.ask(GetCount)   // 等待响应

消息传递模式 ​

基本消息传递 ​

让我们创建一个更完整的示例:一个接收消息并打印的日志 actor。

打印消息的日志 actor

在 Rust 中,你需要创建通道、定义消息枚举、生成线程并手动匹配传入消息。Auto 的 actor 模型将这一切打包成更简洁、专用的抽象。

扇出:多个 Actor,一条消息 ​

一种常见的模式是将消息广播到多个 actor:

扇出:向多个工作 actor 发送消息

管道:顺序处理 ​

消息可以流经一系列 actor,每个 actor 执行一次转换:

actor 管道处理数据

Actor 中的状态管理 ​

每个 actor 维护自己的私有状态。处理消息时,actor 可以修改其状态,这些更改会保留到下一条消息。

累加器模式 ​

累加值的 actor

Auto 版本明显更简单:没有 Arc、没有 Mutex、没有线程管理。Actor 一次处理一条消息,因此 total 上不可能有数据竞争。

具有复杂状态的 Actor ​

键值存储 actor

与 Rust 线程互操作 ​

当你需要使用 Rust 的基于线程的并发时(例如,调用使用线程的 Rust 库),Auto 通过 use.rust 提供访问:

使用 Rust 线程进行互操作

何时使用 Actor vs 线程 ​

场景使用 Actor使用线程
独立服务是否
共享可变状态用 actor 避免Mutex + Arc
CPU 密集型并行有限是
Rust 库互操作否是
基于消息的工作流是可以但冗长
简单后台任务是两者皆可

Send 规范 ​

与 Rust 类似,Auto 使用 Send 规范来表示类型可以在 actor(或线程)之间安全转移。所有权系统确保通过消息传递的值从一个 actor 干净地转移到另一个 actor。

大多数 Auto 类型自动实现 Send。主要例外是包含通过 use.rust 访问的非线程安全 Rust 引用的类型。

消息中的所有权转移 ​

当发送包含值的消息时,该值的所有权转移到接收 actor:

auto
let data = List.of(1, 2, 3)
worker.send(Process(data))
// data 在此处不再有效 — 所有权已转移

这防止发送者意外修改接收者正在使用的数据。所有权系统在编译时捕获此问题。

发送前克隆 ​

如果需要保留副本,在发送前克隆数据:

auto
let data = List.of(1, 2, 3)
worker.send(Process(data.clone()))
// data 在此处仍然有效 — 我们发送了克隆
print(data)  // 正常工作

并发速查表 ​

功能Auto(Actor)Rust(线程)
定义单元actor Name { }thread::spawn(|| { })
发送消息actor.send(Msg)tx.send(val)
请求/响应actor.ask(Msg)rx.recv() + tx.send()
共享状态不共享 — actor 拥有Arc<Mutex<T>>
防止数据竞争设计保证(不共享)所有权 + 类型系统
防止死锁设计保证(无锁)小心的锁排序
创建新单元Name.init()thread::spawn()
标记规范SendSend + Sync

总结 ​

Auto 的 actor 模型提供了一种与 Rust 基于线程的模型根本不同的并发方法:

  1. Actor — 具有私有状态的独立计算单元
  2. 消息传递 — actor 通信的唯一方式;不共享内存
  3. send 和 ask — 即发即忘和请求-响应模式
  4. 所有权转移 — 消息转移所有权,防止数据竞争
  5. 无锁 — actor 一次处理一条消息,消除死锁
  6. Rust 互操作 — 通过 use.rust 可用基于线程的并发

Actor 模型用共享内存的灵活性换取了隔离状态的安全性和简单性。对于大多数并发应用,这种权衡产生的代码更容易编写、更容易理解,并且没有整类并发 bug。

在下一章,我们将探讨 Auto 使用 ~T 蓝图的异步编程模型,以及它与 Rust 的 async/await 的关系。

基于 MIT 许可发布。