为什么官方要重做并发模型?
-
回调地狱
过去写网络层,三步操作(读配置→请求→刷新 UI)要嵌套三层 closure,改起来像"剥洋葱"。
-
数据竞争难查
多个线程同时写同一个 var,80% 崩溃出现在用户设备,本地调试复现不了。
-
结构化生命周期
GCD 的 queue 没有"父-子"关系,任务飞出 App 生命周期后还在跑,造成野任务。
Swift 5.5 引入的 结构化并发(Structured Concurrency) 把"异步"和"并行"收编进语言层:
- 编译期即可发现数据竞争(Data Race)
- 所有异步路径必须标记
await,一眼看出挂起点 - 任务自动形成树形层级,父任务取消,子任务必取消
核心语法 6 连击
| 关键字 | 作用 | 记忆口诀 |
|---|---|---|
async |
声明函数"可能中途睡觉" | 写在参数表后、-> 前 |
await |
调用 async 函数时"可能卡这里" | 必须写,不然编译器报错 |
async let |
并行启动子任务,先跑后等 | "先开枪后瞄准" |
TaskGroup |
动态产生 n 个任务 | 批量下载最爱 |
Actor |
让"可变状态"串行访问 | 自带一把串行锁 |
@MainActor |
让代码只在主线程跑 | UI 必用 |
async/await 最简闭环
swift
// 1️⃣ 把耗时函数标记为 async
func listPhotos(inGallery gallery: String) async throws -> [String] {
try await Task.sleep(for: .milliseconds(Int.random(in: 0...500))) // 模拟网络
return ["img1", "img2", "img3"]
}
// 2️⃣ 调用方用 await 挂起
Task {
let photos = try await listPhotos(inGallery: "Vacation")
print("拿到 \(photos.count) 张图片")
}
注意点
- 只有
async上下文才能调用async函数------同步函数永远写不了await。 - 没有
dispatch_async那种"偷偷后台跑"的魔法,挂起点 100% 显式。
异步序列 ------ 一次拿一条
传统回调"一口气全回来",内存压力大;AsyncSequence 支持"来一个处理一个"。
swift
import Foundation
// 自定义异步序列:每 0.5s 吐一个整数
struct Counter: AsyncSequence {
typealias Element = Int
struct AsyncIterator: AsyncIteratorProtocol {
var current = 1
mutating func next() async -> Int? {
guard current <= 5 else { return nil }
try? await Task.sleep(for: .seconds(0.5))
defer { current += 1 }
return current
}
}
func makeAsyncIterator() -> AsyncIterator { AsyncIterator() }
}
// 使用 for-await 循环
Task {
for await number in Counter() {
print("收到数字", number) // 1 2 3 4 5,间隔 0.5s
}
}
并行下载:async let vs TaskGroup
场景:一次性拉取前三张大图,互不等待。
- async let 写法(任务数量固定)
swift
func downloadPhoto(named: String) async throws -> String {
try await Task.sleep(for: .milliseconds(Int.random(in: 0...500)))
return named
}
func downloadThree() async throws -> [String] {
// 同时启动 3 个下载
async let first = downloadPhoto(named: "1")
async let second = downloadPhoto(named: "2")
async let third = downloadPhoto(named: "3")
// 到这里才真正等待
return try await [first, second, third]
}
- TaskGroup 写法(数量运行时决定)
swift
func downloadAll(names: [String]) async throws -> [String] {
return try await withThrowingTaskGroup(of: String.self) {
group in
for name in names {
group.addTask {
try await downloadPhoto(named: name)
}
}
var results: [String] = []
// 顺序无所谓,先下完先返回
for try await data in group {
results.append(data)
}
return results
}
}
任务取消 ------ 合作式模型
Swift 不会"硬杀"线程,任务要自己检查取消标志:
swift
Task {
let task = Task {
for i in 1...100 {
try Task.checkCancellation() // 被取消会抛 CancellationError
try await Task.sleep(for: .milliseconds(1))
print("第 \(i) 毫秒")
}
}
// 120 毫秒后取消
try await Task.sleep(for: .milliseconds(120))
task.cancel()
}
子任务会大概执行到60毫秒左右,是因为Task开启需要时间
Actor ------ 让"可变状态"串行化
swift
actor TemperatureLogger {
private(set) var max: Double = .leastNormalMagnitude
private var measurements: [Double] = []
func update(_ temp: Double) {
measurements.append(temp)
max = Swift.max(max, temp) // 内部无需 await
}
}
// 使用
let logger = TemperatureLogger()
Task {
await logger.update(30.5) // 外部调用需要 await
let currentMax = await logger.max
print("当前最高温", currentMax)
}
编译器保证:
- 任意时刻最多 1 个任务在
logger内部执行 - 外部访问自动加
await,天然线程安全
MainActor ------ 专为 UI 准备的"主线程保险箱"
swift
@MainActor
func updateUI(with image: UIImage) {
imageView.image = image // 100% 主线程
}
// 在后台任务里调用
Task {
let img = await downloadPhoto(named: "cat")
await updateUI(with: img) // 编译器提醒写 await
}
也可以直接给整个类/结构体加锁:
swift
@MainActor
class PhotoGalleryViewModel: ObservableObject {
@Published var photos: [UIImage] = []
// 所有属性 & 方法自动主线程
}
Sendable ------ 跨并发域的"通行证"
只有值类型(struct/enum)且内部所有属性也是 Sendable,才允许在任务/actor 之间自由传递;
class 默认不 Sendable,除非手动加 @MainActor 或自己实现同步。
swift
struct TemperatureReading: Sendable { // 编译器自动推断
var uuid: UUID
var celsius: Double
}
class NonSafe: @unchecked Sendable { // 自己保证线程安全
private let queue = DispatchQueue(label: "lock")
private var _value: Int = 0
var value: Int {
queue.sync { _value }
}
func increment() {
queue.async { self._value += 1 }
}
}
实战套路小结
- 入口用
Task {}创建异步上下文 - 有依赖关系 → 顺序
await - 无依赖关系 →
async let或TaskGroup - 可变状态 → 收进
actor - UI 刷新 → 贴
@MainActor - 跨任务传值 → 先检查
Sendable
容易踩的 4 个坑
| 坑 | 现象 | 官方建议 |
|---|---|---|
在同步函数里强行 await |
编译直接报错 | 从顶层入口开始逐步 async 化 |
把大计算放进 async 函数 |
仍然卡住主线程 | 用 Task.detached 丢到后台 |
Actor 里加 await 造成重入 |
状态不一致 | 把"读-改-写"做成同步方法 |
| 忘记处理取消 | 用户返回页面还在下载 | 周期 checkCancellation |
扩展场景:SwiftUI + Concurrency 一条龙
swift
struct ContentView: View {
@StateObject var vm = PhotoGalleryViewModel()
var body: some View {
ScrollView {
LazyVStack {
ForEach(vm.photos.indices, id: \.self) { i in
Image(uiImage: vm.photos[i])
.resizable()
.scaledToFit()
}
}
}
.task { // SwiftUI 提供的并发生命周期
await vm.loadGallery() // 离开页面自动取消
}
}
}
@MainActor
class PhotoGalleryViewModel: ObservableObject {
@Published var photos: [UIImage] = []
func loadGallery() async {
let names = await api.listPhotos()
let images = await withTaskGroup(of: UIImage.self) { group -> [UIImage] in
for name in names {
group.addTask { await api.downloadPhoto(named: name) }
}
return await group.reduce(into: []) { $0.append($1) }
}
self.photos = images
}
}
总结 & 展望
Swift 的并发设计把"容易写错"的地方全部做成编译期错误:
- 忘写
await→ 编译失败 - 数据竞争 → 编译失败
- 跨域传非 Sendable → 编译失败
这让大型项目的并发代码第一次拥有了"可维护性"------读代码时,只要看见 await 就知道这里会挂起;看见 actor 就知道内部状态绝对安全;看见 @MainActor 就知道 UI 操作不会蹦到后台线程。