【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 操作不会蹦到后台线程。

相关推荐
东坡肘子15 小时前
毕业 30 年同学群:一场 AI 引发的“真假难辨”危机 -- 肘子的 Swift 周报 #112
人工智能·swiftui·swift
Antonio9151 天前
【Swift】UIKit:UIAlertController、UIImageView、UIDatePicker、UIPickerView和UISwitch
ios·cocoa·swift
Antonio9151 天前
【Swift】UIKit:UISegmentedControl、UISlider、UIStepper、UITableView和UICollectionView
开发语言·ios·swift
1***81531 天前
Swift在服务端开发的可能性探索
开发语言·ios·swift
S***H2831 天前
Swift在系统级应用中的开发
开发语言·ios·swift
HarderCoder2 天前
SwiftUI 状态管理极简之道:从“最小状态”到“状态树”
swift
Antonio9152 天前
【Swift】 UIKit:UIGestureRecognizer和UIView Animation
开发语言·ios·swift
蒙小萌19932 天前
Swift UIKit MVVM + RxSwift Development Rules
开发语言·prompt·swift·rxswift
Antonio9152 天前
【Swift】Swift基础语法:函数、闭包、枚举、结构体、类与属性
开发语言·swift
Antonio9152 天前
【Swift】 Swift 基础语法:变量、类型、分支与循环
开发语言·swift