Swift concurrency 8 — Actor的理解与使用

目录

    • 什么是 Actor?
    • Actor 的定义语法
      • 特点总结:
    • Actor 的作用与好处
      • ✅ 自动避免数据竞争
      • ✅ 明确状态边界
      • ✅ 与 async/await 协同
    • 与 class 的区别
    • 如何使用 Actor
      • 异步访问 Actor 方法
      • 在 actor 内部可以同步访问属性
      • `nonisolated` 用法
    • 使用 actor 的实际场景
      • ✅ 适合使用 actor 的情况:
      • 不适合使用 actor 的情况:
    • Actor 的高级用法与补充
      • 1. `MainActor`
      • 2. Actor 与 protocol
    • Actor 的局限性
    • 总结:什么时候使用 Actor?
    • 最后的建议

Swift 从 5.5 开始引入了基于协程的并发模型(Concurrency Model),为我们带来了 async/await、Task 以及 actor 等新特性。其中, actor 是专门为解决 数据竞争(data race)问题而设计的结构化并发组件。在传统的多线程编程中,开发者常常需要依赖锁(如 DispatchQueue, NSLock 等)来手动同步数据访问,这既容易出错,又难以维护。 actor 的出现,大大简化了并发编程的复杂性。

本文将全面讲解 Swift 中的 actor,包括其定义、使用方式、与 class 的对比、使用场景等,帮助大家更深入地理解并掌握它。


什么是 Actor?

actor 是 Swift 中一种引用类型(reference type),它的主要特性是:它内部的状态是线程隔离的,只有一个任务能在任意时间访问其可变状态。换句话说,Swift 会自动对 actor 实例的访问进行序列化处理,防止数据竞争。

actor Counter {
    private var value = 0

    func increment() {
        value += 1
    }

    func getValue() -> Int {
        return value
    }
}

在上面的示例中,Counter 是一个 actor,它的 value 属性只能通过该 actor 的方法访问,并且 Swift 会自动保证对这些方法的调用是线程安全的。


Actor 的定义语法

定义一个 actor 与定义一个 class 非常相似,只需将关键字 class 替换为 actor 即可:

actor MyActor {
    var state = 0

    func doSomething() {
        state += 1
    }
}

特点总结:

  • 是引用类型(reference type)
  • 具备自动线程安全(通过串行访问)
  • 支持继承协议(但不能继承其他 actor 或 class)
  • 不支持继承(即使是其它 actor)

Actor 的作用与好处

Swift 的 actor 提供了一个更现代、结构化且安全的方式来管理共享状态,避免常见的并发编程陷阱:

✅ 自动避免数据竞争

不需要手动加锁,actor 会将对其内部可变状态的访问自动进行串行化处理。

✅ 明确状态边界

Actor 将可变状态封装在内部,外部只能通过异步方法访问,增强封装性。

✅ 与 async/await 协同

Actor 天然支持 async/await,在异步函数中以直观的方式使用,代码更简洁清晰。


与 class 的区别

虽然 actor 和 class 都是引用类型,它们在并发模型下的行为存在显著差异。

特性 class actor
并发安全 ❌ 需要开发者手动控制 ✅ 自动并发安全
可变状态访问 直接访问 需通过 async 方法访问(默认)
继承支持 ✅ 支持继承 ❌ 不支持继承,只能遵循协议
并发执行行为 不限制,可能产生 data race 内部状态串行访问,避免竞争

这使得在需要并发访问共享状态时,actor 更加合适。


如何使用 Actor

异步访问 Actor 方法

由于 actor 会对外部调用进行隔离,大多数 actor 的方法(非 nonisolated)都需要使用 await 调用。

let counter = Counter()

Task {
    await counter.increment()
    let currentValue = await counter.getValue()
    print("Current value: \(currentValue)")
}

在 actor 内部可以同步访问属性

actor Counter {
    private var value = 0

    func increment() {
        value += 1 // OK:actor 内部访问自身状态不需要 await
    }

    func valuePlusTen() -> Int {
        return value + 10
    }
}

nonisolated 用法

有时候我们希望某个方法可以在不跳出并发隔离的上下文下被访问,这时可以用 nonisolated

actor Logger {
    nonisolated func logInfo(_ msg: String) {
        print("[INFO] \(msg)") // 不访问 actor 内部状态,所以可标注为 nonisolated
    }
}

使用 actor 的实际场景

✅ 适合使用 actor 的情况:

  • 有共享状态并可能在多个 Task 中访问
  • 涉及复杂同步逻辑,且希望避免使用锁
  • 对性能要求不极端苛刻,能容忍 actor 的串行访问开销
  • 想利用结构化并发,提升代码清晰度与可靠性

不适合使用 actor 的情况:

  • 数据量大但读多写少(可以考虑使用无锁数据结构或读写锁)
  • 访问频率极高,对性能有极端要求(actor 存在调度开销)
  • 不涉及并发访问,完全可以用 struct 或 class 实现

Actor 的高级用法与补充

1. MainActor

这是一个特殊的 actor,表示代码必须在主线程执行,常用于 UI 更新或主线程约束逻辑。

@MainActor
class ViewModel {
    var title: String = ""

    func updateTitle(_ newTitle: String) {
        title = newTitle
    }
}

在 SwiftUI 或 UIKit 中操作 UI 时,推荐使用 @MainActor 来保证线程安全。

2. Actor 与 protocol

actor 可以遵循协议,但在定义协议时要注意异步接口的约定:

protocol ScoreUpdatable {
    func increaseScore() async
}

actor GameManager: ScoreUpdatable {
    private var score = 0

    func increaseScore() async {
        score += 1
    }
}

如果协议中包含异步方法,实现时也必须使用 async


Actor 的局限性

虽然 actor 为我们带来了结构化并发和自动的线程安全,但它并非万能:

  • 每次跨 actor 调用都是异步操作,可能有性能开销
  • 不能继承其他 actor 或 class
  • 对于高频读操作,actor 的串行机制可能成为瓶颈

总结:什么时候使用 Actor?

场景 是否适合使用 actor
有状态共享 ✅ 非常合适
多线程并发访问 ✅ 自动隔离访问
无需继承机制 ✅ actor 不支持继承
对性能极端敏感 ❌ 可能影响性能
UI 更新 ✅ 使用 @MainActor

使用 actor 是 Swift 并发编程的重要组成部分,能帮助我们以更安全、简洁的方式编写线程安全代码。虽然不是所有场景都需要 actor,但在状态共享、异步逻辑以及协程结构化控制中,它都是非常有价值的工具。


最后的建议

  • 优先考虑用 struct + immutable state;
  • 如果状态需要在多个任务中共享并变更,用 actor;
  • 对性能敏感的场景,使用 actor 时应做性能测试;
  • 不要滥用 @MainActor,避免主线程拥塞。

掌握 actor 的正确用法,是写好现代 Swift 并发代码的关键一步。如果你还未在项目中尝试 actor,现在是时候开始了。

最后,希望能够帮助到有需要的朋友,如果觉得有帮助,还望点个赞,添加个关注,笔者也会不断地努力,写出更多更好用的文章。

你可能感兴趣的:(Swift,Concurrency,ios,concurrency,async,swift)