【Swift Concurrency】彻底告别回调地狱——async/await、Task、Actor 系统精讲
iOS 进阶必修 · Swift 并发编程系列 第 1 期
一、一句话介绍
Swift Concurrency 是 Apple 在 Swift 5.5(iOS 15+)正式引入的原生并发框架,它让异步代码的编写、错误处理、线程安全变得声明式、结构化、且编译器可静态验证。
| 属性 | 信息 |
|---|---|
| 引入版本 | Swift 5.5 / Xcode 13 |
| 运行时最低要求 | iOS 13+(back-deploy)/ iOS 15+ 全功能 |
| 核心特性 | async/await · Task · Actor · AsyncStream |
| 与 Combine 关系 | 互补共存,AsyncSequence 可与 Combine 互转 |
| 官方文档 | Swift Concurrency |
二、为什么选择它
原生异步方案的痛点
在 Swift Concurrency 出现之前,iOS 异步编程长期面临这些问题:
| 旧方案 | Swift Concurrency |
|---|---|
| 回调嵌套(Callback Hell),可读性极差 | async/await 线性写法,与同步代码几乎一致 |
DispatchQueue + 锁保护共享状态,极易出错 | actor 编译器静态保证线程安全 |
DispatchGroup 聚合多个并行任务,样板代码多 | async let / withTaskGroup 声明式并行 |
| 任务取消需要自行维护 flag,容易遗漏 | 结构化取消,父取消子自动跟随 |
线程切换 DispatchQueue.main.async {} 到处散落 | @MainActor 注解,编译器强制保证主线程 |
Combine 学习曲线陡,操作符多 | AsyncStream 原生支持,与 for await 天然融合 |
核心优势:
- 可读性:async/await 让异步代码读起来像同步,减少 80% 认知负担
- 安全性:actor 让数据竞争成为编译错误而非运行时崩溃
- 结构化:父子任务形成树形结构,取消/错误自动传播
- 可组合:AsyncSequence 统一了事件流、定时器、网络流的消费模型
- 零依赖:语言内置,无需引入任何第三方库
三、核心功能速览
基础层(新手必读)
无需配置,开箱即用
Swift Concurrency 是语言特性,直接在 Xcode 13+ 的任意 Swift 文件中使用:
// Swift 5.5+ · iOS 13+ (back-deploy) / iOS 15+ (全功能)
import Foundation // 仅需标准库
async/await:异步函数的声明与调用
// ✅ 声明异步函数:加 async 关键字
func fetchUser(id: Int) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(id)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
// ✅ 调用:必须在 async 上下文中,用 await 挂起
Task {
do {
let user = try await fetchUser(id: 1)
print(user.name)
} catch {
print("加载失败:\(error)")
}
}
await是挂起点而非阻塞点:挂起时线程被释放,恢复后可能在不同线程继续执行。这是 Swift Concurrency 高效的根本原因。
SwiftUI 中使用 .task 修饰符(推荐)
struct UserView: View {
@State private var user: User?
var body: some View {
Text(user?.name ?? "加载中...")
.task {
// 视图消失时任务自动取消,无需手动管理
user = try? await fetchUser(id: 1)
}
}
}
进阶层(最佳实践)
async let:并行执行多个任务
// ❌ 顺序执行:总耗时 = 500ms + 300ms + 200ms = 1000ms
let user = try await fetchUser(id: 1)
let orders = try await fetchOrders(uid: 1)
let profile = try await fetchProfile(uid: 1)
// ✅ async let 并行:总耗时 = max(500ms, 300ms, 200ms) = 500ms
async let user = fetchUser(id: 1)
async let orders = fetchOrders(uid: 1)
async let profile = fetchProfile(uid: 1)
let (u, o, p) = try await (user, orders, profile)
// 三行代码实现并行,耗时减半
withTaskGroup:动态数量的并行任务
// 并行下载数量不固定的图片列表
func downloadImages(urls: [URL]) async throws -> [UIImage] {
try await withThrowingTaskGroup(of: UIImage.self) { group in
for url in urls {
group.addTask { try await fetchImage(from: url) }
}
var images: [UIImage] = []
for try await image in group {
images.append(image)
}
return images
}
}
Task:非结构化任务与取消
// 创建任务(继承当前 actor 上下文)
let task = Task(priority: .userInitiated) {
for i in 1...100 {
try Task.checkCancellation() // 取消时自动 throw CancellationError
await processItem(i)
}
}
// 取消(协作式,不会强制停止)
task.cancel()
// Task.detached:不继承 actor 上下文,完全独立
Task.detached(priority: .background) {
let result = await heavyComputation()
await MainActor.run { updateUI(result) }
}
Continuation:桥接旧式回调 API
// 将旧式 completion block API 包装为 async 函数
func requestLocation() async throws -> CLLocation {
try await withCheckedThrowingContinuation { continuation in
locationManager.requestLocation { location, error in
if let error {
continuation.resume(throwing: error)
} else if let location {
continuation.resume(returning: location)
}
}
}
}
// ⚠️ resume 只能调用一次,多次调用会 crash
深入层(源码视角)
核心模块职责划分
| 特性 | 职责 | 适用场景 |
|---|---|---|
async/await | 异步函数声明与挂起 | 任何异步 IO 操作 |
async let | 静态数量并行任务 | 首页多接口聚合 |
Task | 非结构化任务单元 | 按钮触发的独立操作 |
withTaskGroup | 动态数量结构化并发 | 批量下载/处理 |
actor | 数据竞争保护 | 共享状态管理 |
@MainActor | 主线程强制约束 | UI 更新 |
Sendable | 跨边界类型安全 | actor 参数/返回值 |
AsyncStream | 自定义异步序列 | 事件流/实时数据 |
四、实战演示
场景:AI 流式问答 + 打字机渲染
这是目前最热门的应用场景之一,完整演示了 AsyncStream + Task + @MainActor 的协同工作。
// Swift 5.5+
// MARK: - 1. 流式 AI 服务层(可替换为真实 SSE 接口)
enum AIStreamService {
/// Mock:逐字符推送,实际项目替换为 URLSession.bytes 读取 SSE
static func stream(prompt: String) -> AsyncStream<String> {
let response = "Swift Concurrency 让并发编程如行云流水," +
"async/await 消除回调地狱,Actor 守护数据安全," +
"AsyncStream 带来流式体验。🚀"
return AsyncStream { continuation in
Task {
for char in response {
guard !Task.isCancelled else {
continuation.finish()
return
}
continuation.yield(String(char))
try? await Task.sleep(nanoseconds: 60_000_000) // 60ms/字
}
continuation.finish()
}
}
}
/// 接入真实 SSE 接口(生产参考)
static func streamFromSSE(url: URL) -> AsyncStream<String> {
AsyncStream { continuation in
Task {
let (bytes, _) = try await URLSession.shared.bytes(from: url)
for try await line in bytes.lines {
guard line.hasPrefix("data: "),
let data = line.dropFirst(6).data(using: .utf8),
let json = try? JSONDecoder().decode(TokenResponse.self, from: data)
else { continue }
continuation.yield(json.token)
}
continuation.finish()
}
}
}
}
// MARK: - 2. SwiftUI 打字机视图
struct TypewriterView: View {
@State private var prompt = "Swift 并发编程"
@State private var output = ""
@State private var isStreaming = false
@State private var streamTask: Task<Void, Never>?
var body: some View {
VStack(alignment: .leading, spacing: 16) {
TextField("输入问题…", text: $prompt)
.textFieldStyle(.roundedBorder)
// 打字机光标效果
Text(output + (isStreaming ? "▌" : ""))
.font(.body)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(Color(.secondarySystemBackground))
.cornerRadius(10)
.animation(.none, value: output)
HStack(spacing: 12) {
Button(isStreaming ? "生成中…" : "开始生成") {
startStream()
}
.buttonStyle(.borderedProminent)
.disabled(isStreaming)
Button("停止") {
streamTask?.cancel()
isStreaming = false
}
.buttonStyle(.bordered)
.tint(.red)
.disabled(!isStreaming)
}
}
.padding()
.onDisappear { streamTask?.cancel() } // ✅ 离开页面时取消
}
private func startStream() {
streamTask?.cancel()
output = ""
isStreaming = true
streamTask = Task {
for await token in AIStreamService.stream(prompt: prompt) {
output += token // SwiftUI 自动感知变化实时渲染
}
isStreaming = false
}
}
}
// MARK: - 3. UIKit 打字机控制器(@MainActor 保证 UI 安全)
@MainActor
class TypewriterViewController: UIViewController {
private let textView = UITextView()
private var streamTask: Task<Void, Never>?
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
streamTask?.cancel() // ✅ 离开页面时取消,防止内存泄漏
}
@objc func startStream() {
streamTask?.cancel()
textView.text = ""
streamTask = Task {
for await token in AIStreamService.stream(prompt: "UIKit") {
guard !Task.isCancelled else { break }
textView.text += token
// 自动滚到底部
let range = NSRange(location: textView.text.count - 1, length: 1)
textView.scrollRangeToVisible(range)
}
}
}
}
这个示例完整演示了:AsyncStream 的创建与消费、Task 的取消管理、@MainActor 的 UI 安全保证、SwiftUI 和 UIKit 的两套接入方式。
五、源码亮点
进阶层:值得借鉴的设计
Actor 并发计数器(告别 DispatchQueue + 锁)
// ❌ 传统写法:容易因忘记加锁而出现数据竞争
class Counter {
var value = 0
let queue = DispatchQueue(label: "counter.queue")
func increment() { queue.sync { value += 1 } }
}
// ✅ actor:编译器静态保证,忘加 await 直接报错
actor SafeCounter {
private(set) var value = 0
func increment() { value += 1 }
}
// 并发使用:1000 个任务同时递增,结果一定是 1000
let counter = SafeCounter()
await withTaskGroup(of: Void.self) { group in
for _ in 0..<1000 {
group.addTask { await counter.increment() }
}
}
print(await counter.value) // 1000,绝无数据竞争
AsyncStream 资源安全回收
// 定时器流:onTermination 防止 timer 泄漏
func timerStream(interval: Double) -> AsyncStream<Int> {
AsyncStream { continuation in
var tick = 0
let timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { _ in
tick += 1
continuation.yield(tick)
}
// ✅ 流取消/结束时自动调用,清理外部资源
continuation.onTermination = { _ in
timer.invalidate()
}
}
}
深入层:设计思想解析
结构化并发:任务树模型
Swift Concurrency 引入了"结构化并发"概念——任务形成父子树形结构:
父任务(Task)
├── 子任务 A(async let)
├── 子任务 B(async let)
└── TaskGroup
├── 子任务 C(addTask)
└── 子任务 D(addTask)
关键特性:
- 父取消 → 子自动取消:无需手动遍历
- 子抛出错误 → 父捕获:错误自动冒泡
- 父作用域结束 → 等待所有子完成:无任务泄漏
这与 Kotlin 协程的 StructuredConcurrency 思想一脉相承,但 Swift 通过编译器强制实施,更难写错。
Actor 的可重入设计
Actor 内部通过隐式串行队列保证数据安全,但它是可重入的:
actor BankAccount {
var balance: Double = 1000
// ⚠️ 重入陷阱:await 挂起期间,其他任务可进入 actor 修改 balance
func withdrawUnsafe(amount: Double) async throws {
guard balance >= amount else { throw BankError.insufficient }
await logTransaction(amount) // 挂起!balance 可能被别的 withdraw 修改
balance -= amount // 此时 balance 可能已不足!
}
// ✅ 正确:先修改状态再 await
func withdrawSafe(amount: Double) async throws {
guard balance >= amount else { throw BankError.insufficient }
balance -= amount // 先扣,在 await 之前完成关键状态变更
await logTransaction(amount)
}
}
规则:actor 中,await 之前必须完成所有关键状态变更。
六、踩坑记录
问题 1:Continuation.resume 调用了多次导致 crash
- 原因:某些旧 SDK 的 completion block 可能被调用多次(如进度回调)
- 解决:用 bool flag 保护,确保 resume 只执行一次
func safeContinuation<T>(_ block: (@escaping (T) -> Void) -> Void) async -> T {
await withCheckedContinuation { continuation in
var resumed = false
block { value in
guard !resumed else { return }
resumed = true
continuation.resume(returning: value)
}
}
}
问题 2:Task.detached 中直接更新 UI 导致崩溃
- 原因:
Task.detached不继承当前 actor 上下文,不在主线程 - 解决:显式切回主线程
// ❌ 危险
Task.detached { self.label.text = "done" }
// ✅ 正确
Task.detached {
let result = await process()
await MainActor.run { self.label.text = result }
}
问题 3:视图消失后 Task 仍在运行,导致内存泄漏
- 原因:Task 生命周期独立于视图,视图销毁后任务仍持有 self
- 解决:SwiftUI 用
.task {}修饰符(自动管理),UIKit 在viewWillDisappear中 cancel
// UIKit
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
loadTask?.cancel()
}
问题 4:Actor 重入性导致余额多扣
- 原因:await 挂起期间其他任务进入 actor 修改共享状态
- 解决:遵守"先修改状态,再 await"原则(见第五章深入层)
问题 5:AsyncStream 中 timer / 监听器未释放,持续运行
- 原因:忘记实现
continuation.onTermination - 解决:每个 AsyncStream 必须实现
onTermination,清理外部资源
continuation.onTermination = { reason in
timer.invalidate()
notificationCenter.removeObserver(observer)
}
问题 6:withTaskGroup 中子任务抛出错误没有被感知
- 原因:使用了
withTaskGroup(不抛出版),错误被吞掉 - 解决:需要错误传播时,使用
withThrowingTaskGroup
// ✅ 任意子任务失败,整个 group 取消并抛出错误
try await withThrowingTaskGroup(of: Data.self) { group in
for url in urls { group.addTask { try await fetch(url) } }
for try await data in group { process(data) }
}
问题 7:在 iOS 13 / 14 上使用 actor 报链接错误
- 原因:actor 运行时需要 iOS 15+ 的系统库支持;Xcode back-deploy 支持 async/await 但不完全支持 actor
- 解决:确认最低 Deployment Target,或对 actor 用
@available(iOS 15, *)包裹
七、延伸思考
与同类方案横向对比
| 方案 | 简介 | 学习曲线 | 线程安全 | 取消支持 | 适用场景 |
|---|---|---|---|---|---|
| Swift Concurrency | Swift 原生,语言级别支持 | 中 | 编译器保证(actor) | 结构化取消 | 新项目首选 |
| GCD + DispatchQueue | 苹果传统并发方案 | 低 | 手动加锁,容易出错 | 无原生支持 | 老项目维护 |
| Combine | 响应式框架,操作符丰富 | 高 | 需手动 receive(on:) | AnyCancellable | 复杂数据流转换 |
| PromiseKit | 基于 Promise 的链式回调 | 中 | 无特殊支持 | 有限支持 | OC/早期 Swift 项目 |
| RxSwift | 响应式编程全家桶 | 很高 | 需配置 scheduler | Disposable | 重度响应式架构 |
推荐使用场景
- ✅ iOS 13+ 新项目,全面拥抱 Swift Concurrency
- ✅ 需要并行聚合多个接口的页面(async let / TaskGroup)
- ✅ 共享状态管理,替代 DispatchQueue + 锁(actor)
- ✅ 实时数据流、WebSocket、AI 流式响应(AsyncStream)
- ✅ 需要优雅取消的长时任务(下载、文件处理)
不推荐场景
- ❌ 项目最低支持 iOS 12 及以下,部分特性无法使用
- ❌ 已有大量 Combine 代码,短期内迁移成本过高
- ❌ 需要复杂响应式操作符链(merge、combineLatest 等),Combine 更合适
迁移策略建议
- 新功能优先用 async/await,不强制改旧代码
- 旧接口用
Continuation包装,对调用方透明 - Combine Pipeline 可通过
.values属性转为AsyncSequence互通 - Swift 6 开启严格并发检查(
-strict-concurrency=complete),提前消灭隐患
八、参考资源
- Swift 官方文档:Concurrency
- Apple Developer:Swift Concurrency
- WWDC 2021 Meet async/await in Swift
- WWDC 2021 Explore structured concurrency in Swift
- WWDC 2021 Protect mutable state with Swift actors
- WWDC 2022 Eliminate data races using Swift Concurrency
- WWDC 2023 Beyond the basics of structured concurrency
- SE-0296 async/await Proposal
- 系列 Demo 仓库(持续更新):
github.com/yourname/ios-swift-concurrency-demos
九、本期互动
小作业
基于本文的 AsyncStream 示例,实现一个实时心跳检测器:
- 用
AsyncStream每隔 1 秒 yield 一次当前时间戳 - 连续 5 次 yield 后,主动调用
continuation.finish()结束流 - 在 SwiftUI 中用
.task {}消费流,将每次时间戳展示在列表中 - 点击「停止」按钮时,通过
task.cancel()终止流,并验证onTermination被调用
完成后在评论区贴出你的 AsyncStream 创建代码和 onTermination 实现,说明你是如何验证资源正确释放的。
思考题
Swift Concurrency 的 actor 选择了可重入设计——即 await 挂起时允许其他任务进入 actor 执行。你认为这个设计决策是合理的吗?
如果设计成不可重入(像传统锁一样),会带来哪些问题?可重入又会引发哪些坑?在你的实际项目中,你更希望哪种行为?
读者征集
下一期预计深入讲解 Swift Concurrency 进阶:自定义 AsyncSequence、结构化并发原理、Swift 6 严格并发检查实战。
如果你在项目中迁移到 Swift Concurrency 时踩过坑(特别是 actor 重入、Sendable 编译报错、Task 泄漏等),欢迎评论区留言,优质踩坑经历将收录进下一期《踩坑记录》章节!
📅 本系列持续更新 ➡️ 第 1 期:Swift Concurrency 基础精讲(本期)· ○ 第 2 期:Swift Concurrency 进阶 · ○ 第 3 期:待定 · ○ 第 4 期:待定