19.6. Actors

跟类一样,actor 是一个引用类型,所以 类是引用类型 中关于值类型和引用类型的比较同样适用于 actor 和类。不同于类的是,actor 在同一时间只允许一个任务访问它的可变状态,这使得多个任务中的代码和一个 actor 交互时更加安全。比如,下面是一个记录温度的 actor:

actor TemperatureLogger {
    let label: String
    var measurements: [Int]
    private(set) var max: Int
    init(label: String, measurement: Int) {
        self.label = label
        self.measurements = [measurement]
        self.max = measurement
    }
}

你可以用 actor 关键字引入一个 actor,后边的花括号中是它的定义。TemperatureLogger 中有外部能访问到的属性,并且限制 max 变量,所以只能在 actor 内部修改最大值。

你可以使用与结构体和类初始化相同的语法创建一个 actor。当你访问 actor 中的属性或方法时,需要使用 await 来标记潜在的悬点,比如:

let logger = TemperatureLogger(label: "Outdoors", measurement: 25)
print(await logger.max)
// 输出 "25"

在这个例子中,访问 logger.max 是一个可能的悬点。因为 actor 在同一时间只允许一个任务访问它的可变状态,如果别的任务正在与 logger 交互,上面这段代码将会在等待访问属性的时候被挂起。

相比之下,actor 内部的代码在访问其属性的时候不需要添加 await 关键字。比如,下面的方法是更新 TemperatureLogger 中的温度:

extension TemperatureLogger {
    func update(with measurement: Int) {
        measurements.append(measurement)
        if measurement > max {
            max = measurement
        }
    }
}

update(with:) 方法本来就在 actor 中运行,所以没必要在访问如 max 等属性的时候加 await 关键字。这个方法也展示了为什么要在同一时间只允许一个任务访问其可变状态的其中一个理由:一些对于 actor 状态的改变暂时打破了不可变性。 TemperatureLogger 记录了一个温度的列表和最高温度,并且会在你更新了一个新测量值之后更新最大温度。在更新的过程中,在增加了新测量值但没有更新 max 前,TemperatureLogger 正处于一个暂时不一致的状态。阻止不同的任务和同一个 actor 实例交互可以防止一下事件序列的发生:

  1. 你的代码调用 update(with:) 方法,并且先更新了 measurements 数组。
  2. 在你的代码更新 max 前,其他地方的代码读取了最大值和温度列表的值。
  3. 你的代码更新了 max 完成调用。

在这种情况下,其他的代码读取到了错误的值因为这次对于 actor 的读取操作被夹在 update(with:) 方法中间,而且是的数据正好是无效的。你可以用 Swift 中的 actor 以防止这种问题的发生,因为 actor 只允许同一时间内只有一个任务能访问它的状态,而且只有在被 await 标记为悬点的地方代码才会被打断。因为 update(with:) 方法没有任何悬点,没有其他任何代码可以在更新的过程中访问到数据。

如果你想在 actor 外部像访问类属性一样访问 actor 的属性,会得到一个编译时错误;比如:

print(logger.max)  // 报错

不添加 await 关键字的情况下访问 logger.max 会失败,因为 actor 的属性是它隔离的本地状态的一部分。Swift 可以保证只有 actor 内部的代码可以访问 actor 的内部状态。这个保证也被称为 actor isolation