首页 / Swift 编程语言教程 / actor 与数据隔离

Swift 编程语言教程

actor 与数据隔离

本教程共 93 篇 · 第 83 篇 · 更新于 2026-08-08 · 约 9 分钟阅读

SwiftSwift 编程语言教程并发actor数据隔离数据竞争引用类型actor隔离

本节目标:理解 actor 怎么用”一次只让一个任务访问”的串行规则保护共享可变数据,掌握它的声明语法和跨域访问必须写 await。

并发最难搞的问题叫数据竞争(data race):多段代码同时改同一份可变数据,结果取决于谁先谁后,错得毫无规律。Swift 给出的解法之一是 actor。它像个有门禁的保险柜:同一时刻只放一个任务进去翻弄里面的数据,其他人得排队。这一章把 actor 讲透。

1-1 actor 是什么

actor 是和类很像的引用类型——用 actor 关键字定义,可以有属性、方法、初始化器。它最大的不同在于:actor 保证自己内部的可变状态同一时刻只被一个任务访问。多个任务想碰同一个 actor 实例,必须轮流来。

正是这份”轮流”,让 actor 天然免疫数据竞争。你不用自己加锁、不用操心排队,Swift 和运行时替你管。

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
    }
}

这个 TemperatureLogger actor 记录一系列温度并维护最大值。maxprivate(set) 限制成”外部只能读、内部才能改”,把写入权限收在 actor 内部。

1-2 访问 actor 要写 await

创建 actor 实例的语法和结构体、类一样。但访问它的属性或方法时,因为可能要”排队等轮到”,所以必须写 await

let logger = TemperatureLogger(label: "室外", measurement: 25)
print(await logger.max)   // 打印 25

await logger.max 是一个潜在挂起点:如果此刻正有别的任务在跟这个 actor 交互,当前代码就挂起,等轮到它再继续。这个 await 不是可有可无的装饰,而是 actor 隔离的硬性要求。

Warning

若你漏写 await 直接访问 actor 属性,比如写 print(logger.max),编译器会直接报错。这不是语法啰嗦,而是提醒你:跨过 actor 边界是异步的、要排队的。

1-3 actor 内部的代码不写 await

在 actor 内部的方法里访问自己的属性,不需要 await——因为这段代码本来就已经”在 actor 上”运行了,不存在跨边界。

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

update(with:) 是 actor 的方法,运行在 actor 上,所以直接读写 measurementsmax,不写 await。这正体现了 actor 隔离:只有 actor 自己的代码能直接碰内部状态。

1-4 为什么一次只允许一个任务

关键价值在于保护”临时不一致”的状态。看 update 的中间过程:先 append 新温度,再更新 max。在追加完、还没改 max 的瞬间,数据处于不一致的临时态。如果允许别的任务在这时插进来读,就会读到错误的”数组已更新但最大值没跟上”的脏数据。

actor 保证同一时刻只有一个任务在跑,而 actor 内部的同步方法没有挂起点,所以不会在半路被打断。这样”临时不一致”对外完全不可见——别的任务要么在你开始前读,要么在你完整改完后再读,绝不会卡在中间。

Note

想让一段更新绝对不被插断,就把它写成同步方法(不含 await)。同步方法里没有潜在挂起点,Swift 保证它一气呵成跑完。这正是把临时不一致的逻辑封装成同步方法的好处。

1-5 外部直接访问会编译报错

actor 把可变状态当成”隔离的局部状态”,规则很硬:只有运行在 actor 上的代码才能直接访问。外部代码若像访问普通结构体那样直接读,编译不过:

print(logger.max)   // 错误:必须写 await,且不能绕过隔离

这条限制就是 actor 隔离(actor isolation)。它从语言层面堵死了”绕过排队直接偷看/偷改”的可能,把数据安全焊死在类型系统里。对比之下,类没有这层保护,多个线程同时改一个类的属性就会出数据竞争,而你往往要等到运行时才发现问题。

1-6 用 actor 改造计数器的经典例子

数据竞争最经典的演示是多个任务同时给一个计数器 += 1。用普通类会丢更新,用 actor 则安全:

actor SafeCounter {
    private var value = 0
    func increment() { value += 1 }
    func get() -> Int { value }
}

let counter = SafeCounter()

Task {
    await withTaskGroup(of: Void.self) { group in
        for _ in 0..<1000 {
            group.addTask { await counter.increment() }
        }
    }
    print("最终值:\(await counter.get())")   // 稳定输出 1000
}

一千个子任务并发调用 increment(),因为 actor 一次只放一个进去改 value,每个 +1 都完整生效,最终结果稳稳是 1000。换成普通类,几乎必然少于 1000,且每次运行结果还不一样——这就是数据竞争的特征。

Tip

只要一段可变状态会被多个并发任务共享修改,优先把它封装进 actor。这是 Swift 并发里防数据竞争最省心的一招,比自己用锁清晰得多。

1-7 主 actor 是特殊的 actor

Swift 已经为你准备好一个最重要的 actor——主 actor(main actor),它保护所有跟界面(UI)相关的数据。第 85 章会专门讲。你现在只需记住:actor 不是只能自己定义,系统级的”主 actor”就是个全局单例,几乎所有 UI 更新都跑在它上面。

1-8 actor 与类的本质区别

把 actor 和类放在一起比,能看清它的定位。两者都是引用类型,都支持属性、方法、初始化器。但类允许多个线程同时改它的属性,得你自己加锁防竞争;actor 在语言层面强制”一次一个任务”,锁的事你完全不用管。

也正因为这份强制,actor 不能像类那样随意继承(actor 不能被类继承,反之亦然),且它的隔离状态只能由运行在它身上的代码访问。这些约束看似麻烦,实则是把”并发安全”焊进了类型系统——你没法绕过去犯错。

Tip

选型时这样想:这段可变状态会被多个并发任务共享改吗?会 → 用 actor。不会、且只想传值 → 用结构体(值语义+Sendable 更轻)。类在并发时代的使用场景,正在被这两者瓜分。

1-9 小结

actor 是带”串行门禁”的引用类型,靠”同一时刻只让一个任务访问内部状态”根除数据竞争。声明用 actor 关键字;从外部访问它的属性/方法必须写 await(潜在挂起排队);内部的同步方法直接访问、不写 await,且因无挂起点保证不被中途打断。actor 隔离被强制在编译期,比手动加锁安全得多。

下一章讲 Sendable:当数据要在不同并发域(任务、actor)之间传递时,怎样保证它是”可安全发送”的。