无状态 Actors

Hacker News Top 新闻

摘要

对 Swift 中无状态 Actors 的技术探讨,讨论其用途、权衡以及与使用并发函数的结构体的比较,包括网络客户端和后台 Actors 等示例。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/05/30 19:28

# 无状态 Actor 来源:https://www.massicotte.org/stateless-actors/ 2026年5月29日 最近有人问了我一个有趣的问题:如果 Actor 的目的是保护可变状态,那么无状态的 Actor 是否毫无意义? 起初我觉得答案很简单。Actor 的存在是为了在状态周围定义一个小小的保护气泡。它们将数据与任何不安全访问"隔离"开来。一个无事可隔离的 Actor 听起来很奇怪。 这样的安排能发挥作用吗? **注** 我写了另一篇关于 [Actor](https://www.massicotte.org/actors) 的文章,可能也值得一读。 ## 轻松的非 MainActor 类型 我时不时会遇到一种"NetworkClient"风格的类型。它包含处理某些网络 API 的方法。这类类型常常被设计为 Actor。 ``` actor NetworkClient { func loadCart() async throws -> [Product] { let (data, _) = try await URLSession.shared.data(for: cartRequest) return try JSONDecoder().decode([Product].self, from: data) } } ``` 这个特定的 `NetworkClient` 是一个 Actor,没有状态。但作为 Actor 给它带来了两个优势。首先,Actor 类型是 `Sendable` 的。这意味着我们可以轻松地传递这个类型,而无需过多思考。 其次,这个 `loadCart` 方法*绝不*会在主线程上运行。默认的 Actor 类型会在共享线程池上执行它们同步的工作。这里的 JSON 解码可能会很耗时。确保这项工作永远不会发生在主线程上是很棒的。 可能有点牵强,但第三个可信的理由是,这个类型可能只是**尚未**拥有状态。我们最终可能会添加缓存或认证,而这些都需要一个地方来存放。 (有人指出,提前预测状态可能存放在哪里本身可能是有问题的。我大致同意,所以在这里要格外小心。) 我认为这是可以的,**前提**是你是刻意这样做的。只要你理解了权衡,那就去做。但这里确实存在权衡。最值得注意的是,Actor 类型很难甚至不可能与协议一起使用。而且,它们还要求**同时**确保方法的输入和输出可以安全地传入/传出 Actor。它们会迫使你需要更多 `Sendable` 类型。 现在,对比一下: ``` struct NetworkClient: Sendable { @concurrent func loadCart() async throws -> [Product] { let (data, _) = try await URLSession.shared.data(for: cartRequest) return try JSONDecoder().decode([Product].self, from: data) } } ``` 这个类型有一些优点。首先,它更容易与协议一起使用,因为你不需要处理隔离不匹配的问题。 第二个问题是一个人为的限制。Actor 串行地执行同步代码块。这意味着无论你向这个 `NetworkClient` Actor 抛出多少个任务,它一次只能解码一个 JSON 响应。而使用 `@concurrent` 函数,我们不再有这个限制。 我绝对不是说结构体在这里更好。但这里存在严重的权衡,值得思考。 **注** 查看这篇[文章](https://www.massicotte.org/synchronous-work)以获取更多关于如何管理耗时工作的信息。 ## 后台 Actor 这是一个有趣的类型! ``` @globalActor actor BackgroundActor { static let shared = BackgroundActor() } ``` 然后,你可以在所有支持全局 Actor 注解的地方使用它。 ``` Task { @BackgroundActor in // 肯定不再是 @MainActor } ``` 我理解。我们习惯了看到单一的全局 `@MainActor`。这给了我们一种熟悉的方式来定义非主线程的工作。但这样的类型有两个严重的缺点。 就像我们上面的 `NetworkClient` Actor 一样,这个 `BackgroundActor` 串行地执行同步工作。它一次只能运行一个后台任务。对于这样的工具来说,这不是一个理想的特性。 另一个问题是全局 Actor 与类型系统的集成非常紧密。当你添加一个全局 Actor 时,你是在强制编译器保证工作在该 Actor 上执行。这可能会产生病毒式传播的效果,这是很多人在使用 `MainActor` 时注意到的。反过来也可能如此。如果你后来改变主意,**移除**一个全局 Actor 也可能很痛苦。 我理解这些动机。但我真心认为,花点时间学习语言中现有用于控制隔离的构造会对你有最大的帮助。无论如何,我也不确定这是否真的可以省略,所以这似乎是一项不错的投资。 ## 自定义执行器的 Actor 我不知怎么忘记了这一点,但多亏了 [Gwendal](https://hachyderm.io/@groue) 没让我蒙混过关! 这不是你每天都会用到的东西,但那些纯粹为了将 Swift 的并发系统与另一个已有系统适配而存在的 Actor 非常重要。这是[自定义执行器](https://developer.apple.com/documentation/swift/serialexecutor)的主要用例之一。 这是一个强大的工具,并且与 Dispatch Queue 一起使用时出奇地简单。我见过它被用来更好地与 [AVFoundation](https://developer.apple.com/documentation/avfoundation) 集成。但这种方法也可能适用于任何其他基于队列的系统。 请允许我从[迁移指南](https://www.swift.org/migration/documentation/swift-6-concurrency-migration-guide/incrementaladoption#Integrating-DispatchSerialQueue-with-Actors)中摘取一个例子: ``` actor LandingSite { private let queue = DispatchSerialQueue(label: "something") nonisolated var unownedExecutor: UnownedSerialExecutor { queue.asUnownedSerialExecutor() } func acceptTransport(_ transport: PersonalTransportation) { // 这个函数将在 queue 上运行 } } ``` 另一个例子——一个我相当尴尬自己没想起来的例子——就是 `MainActor`!这个 Actor 没有直接的**属性**,但它确实管理着状态——整个 UI。所以它有点介于两者之间,但考虑起来仍然非常有趣。 ## 文件系统 好吧,这个很有趣。文件系统**绝对**是一种状态形式。但它是在程序之外的状态,编译器完全不可见。在这种情况下,我认为"无状态"的 Actor 是有意义的。状态不一定非得是实例属性。 假设你有某种磁盘上的缓存,被系统中许多不同的部分使用。并发访问可能会破坏涉及的文件/目录。Actor 提供的串行化给了你一种防止这种情况的方法。这不是理想的方案,因为编译器无法检查你的工作。要正确实现,确实需要手动封装所有内容,但无论如何你可能都想这样做。 这里可能出现的一个问题是阻塞操作。当你读取/写入磁盘时,你是在同步执行。这意味着这个 Actor 会占用并发运行时的一个线程。这些线程是**有限**的资源,大概等于每个优先级级别的 CPU 核心数。这与 GCD 截然不同,GCD 在没有可用线程时会愉快地创建大量(虽然最终仍然有限)的线程。 我的观点是,通常你不需要担心这个问题。当然,一个线程被占用了。但它被占用**做什么**通常不是什么重要的细节。只要工作满足运行时对"向前推进"的要求,你应该没问题。 然而,如果你担心(或者确实知道)这会有问题,将你的阻塞工作移出并发线程池是有意义的,而且通常很容易做到。GCD 仍然可用,你不应该害怕使用它。 (我忘了 [Jaim](https://bsky.app/profile/jaimzuber.com) 也[写过](https://jaimzuber.com/swift-concurrency/empty-global-actors)关于这个的内容,并且讲得相当详细。值得一看。) ## Actor 的第一条规则 Actor 有一种被过度使用的倾向。它们是非常有用的工具,我更喜欢它们而不是锁或队列。但就像任何同步原语一样,你应该能够清晰地说明为什么它是必要的。这就是 Actor 的第一条规则。 我认为**总的来说**,是的。一个没有状态的 Actor 是奇怪的东西。它可能代表一种误解。它可能让设计变得更复杂。但我认为它们肯定也可以是有意义的。 你知道我为并发和 Swift 6 迁移提供咨询服务吗?如果你觉得我能帮上忙,请[联系我](https://www.massicotte.org/consulting)。

相似文章

Show HN: 我用Swift构建了LangGraph

Hacker News Top

Swarm是一个用于构建代理工作流和多智能体系统的Swift框架,具备类型安全的工具调用、持久化检查点以及对多种LLM提供商的支持。