【Swift 并发编程入门】——从 async/await 到 Actor,一文看懂结构化并发

为什么官方要重做并发模型?

  1. 回调地狱

    过去写网络层,三步操作(读配置→请求→刷新 UI)要嵌套三层 closure,改起来像"剥洋葱"。

  2. 数据竞争难查

    多个线程同时写同一个 var,80% 崩溃出现在用户设备,本地调试复现不了。

  3. 结构化生命周期

    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

场景:一次性拉取前三张大图,互不等待。

  1. 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]
}
  1. 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 }
    }
}

实战套路小结

  1. 入口用 Task {} 创建异步上下文
  2. 有依赖关系 → 顺序 await
  3. 无依赖关系 → async letTaskGroup
  4. 可变状态 → 收进 actor
  5. UI 刷新 → 贴 @MainActor
  6. 跨任务传值 → 先检查 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 操作不会蹦到后台线程。

相关推荐
HarderCoder1 天前
Swift 中的不透明类型与装箱协议类型:概念、区别与实践
swift
HarderCoder1 天前
Swift 泛型深度指南 ——从“交换两个值”到“通用容器”的代码复用之路
swift
东坡肘子1 天前
惊险但幸运,两次!| 肘子的 Swift 周报 #0109
人工智能·swiftui·swift
胖虎11 天前
Swift项目生成Framework流程以及与OC的区别
framework·swift·1024程序员节·swift framework
songgeb2 天前
What Auto Layout Doesn’t Allow
swift
YGGP2 天前
【Swift】LeetCode 240.搜索二维矩阵 II
swift
YGGP3 天前
【Swift】LeetCode 73. 矩阵置零
swift
非专业程序员Ping4 天前
HarfBuzz 实战:五大核心API 实例详解【附iOS/Swift实战示例】
android·ios·swift
Swift社区5 天前
LeetCode 409 - 最长回文串 | Swift 实战题解
算法·leetcode·swift