为什么并发在 Swift 里变了天

在 Swift 5.5 之前,写并发要么靠 GCD 闭包层层嵌套,要么靠手动加锁。回调一多就形成"金字塔",锁一用错就数据竞争。Swift 现代并发从语言层面换掉了这套:用 async/await 写线性代码,用 Actor 自动隔离共享状态,数据竞争从"靠人小心"变成"编译器帮忙盯着"。

现代并发的目标是:让你写出看起来像顺序执行的代码,同时在 await 处自动让出线程;让共享数据被 Actor 保护,少写锁、少出错。

这篇文章沿着"语法 → 并发单元 → 状态隔离 → 实战"的顺序,把这套新模型走一遍。

async/await:把异步写成同步

用 async 标记一个可异步执行的函数,调用处用 await 等待结果。代码从上到下线性书写,读起来和同步函数一样,但每个 await 都是一个挂起点——在那里线程可以去干别的事。

struct User: Decodable { let id: Int; let name: String }

func fetchUser(id: Int) async throws -> User {
    let url = URL(string: "https://example.com/users/\(id)")!
    let (data, resp) = try await URLSession.shared.data(from: url)
    guard let http = resp as? HTTPURLResponse, http.statusCode == 200 else {
        throw URLError(.badServerResponse)
    }
    return try JSONDecoder().decode(User.self, from: data)
}

关键认知:await 不会阻塞线程,只是挂起当前任务。因此可以放心在主线程附近写,不用担心卡死界面。但要记住,await 之后不能假设共享状态没变——别的任务可能在这期间改过它。

Task 与结构化并发

要在同步上下文里启动异步工作,用 Task { }。而要并发执行多个任务并等它们全部完成,用 async let 或 TaskGroup。结构化并发的含义是:父任务会等所有子任务结束,子任务抛出的错误会沿层级传播,不会"发出去就没人管"。

func loadProfile(id: Int) async throws -> (User, [Post]) {
    async let user = fetchUser(id: id)      // 两个请求并发进行
    async let posts = fetchPosts(id: id)
    return try await (user, posts)          // 同时等待两者完成
}
结构化并发要点
  • async let声明即并发,await 处才等待结果,适合数量固定的多个任务。
  • TaskGroup需要动态数量、限制最大并发时用。
  • 父子同生共死父任务取消,子任务一并取消。
  • 避免孤立 Task零散 Task{} 难以管理生命周期,优先在结构化作用域里发起。

Actor:隔离共享可变状态

并发出问题,多半是多个线程同时改同一块数据。Actor 的解法是:把这块数据和它的方法封进一个 Actor,Actor 内部同一时刻只允许一个任务访问,外部访问要 await 排队。锁的工作交给了运行时。

actor Counter {
    private var value = 0
    func next() -> Int {
        value += 1
        return value
    }
}

let c = Counter()
let a = await c.next()   // 跨 Actor 访问必须 await
let b = await c.next()
Swift 并发明示:多条并行发光流线从左上向右下汇聚又分叉,中央是一个守护性的圆形核心节点,流线交织成网
结构化并发:多条任务流并发执行,核心节点(Actor)守护共享状态,互不相撞。

把可变的计数器、缓存池、会话状态放进 Actor,就不用自己加锁了。值类型 struct 天然不共享,跨线程传递安全;真正需要共享的可变对象,才需要 Actor。

@MainActor 与界面更新

界面只能在主线程更新。Swift 用 @MainActor 标注"这段代码必须跑在主线程"。在 SwiftUI 里,视图的 body 默认就在 MainActor 上;网络请求放后台,拿到结果后再回到主线程刷新界面。

实践上推荐这样分工:ViewModel 在后台做网络与计算,把结果交给一个 @MainActor 的属性或方法更新界面。不要在后台线程直接改 SwiftUI 状态,否则会偶发不刷新甚至崩溃。