【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 高效的根本原因。
深入理解:await 挂起 vs 传统回调的线程行为
这是理解 Swift Concurrency 为何高效的关键,也是很多人初学时最容易混淆的地方。
传统 GCD 回调的线程行为
// 传统方式:调用线程不阻塞,但"上下文"从此丢失
func fetchData(completion: @escaping (Data) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, _ in
// 回调所在线程:URLSession 内部线程(不确定,通常是子线程)
completion(data!)
}.resume()
}
// 调用方
fetchData { data in
// ⚠️ 线程已改变,需要手动切回主线程
DispatchQueue.main.async {
self.label.text = "done" // 上下文全靠开发者自己管理
}
}
// 调用方线程立即继续往下跑(不等待,也不挂起)
print("这行代码立即执行,不等 fetchData 完成")
async/await 的线程行为
// async/await:await 是挂起点,调用线程被释放回线程池
func fetchData() async -> Data { ... }
func loadPage() async {
print("当前线程:\(Thread.current)") // 线程 A
let data = await fetchData() // ← 挂起点:线程 A 被释放,可去执行其他任务
// 恢复后:可能是不同线程,但 Actor 上下文(如 @MainActor)被自动还原
print("恢复线程:\(Thread.current)") // 可能是线程 B,但上下文依然正确
updateUI(data) // 如果在 @MainActor 中,编译器保证这里一定在主线程
}
两者最本质的区别:线程是否被"占用"
| 维度 |
传统 GCD 回调 |
async/await |
| 调用方线程 |
继续运行(不等待,不挂起) |
挂起,线程释放回线程池 |
| 等待期间 |
调用线程去干别的事(但无连接) |
线程被其他任务复用 |
| 回调/恢复线程 |
由 GCD 队列决定,不确定 |
由调度器决定,保留 Actor 上下文 |
| 代码连续性 |
回调嵌套,逻辑分散 |
线性代码,逻辑连续 |
| 线程安全 |
手动管理,容易出错 |
编译器 + Actor 静态保证 |
⚠️ 注意:传统回调的调用方线程确实不阻塞,这点和 await 一样。但两者的关键区别在于:传统回调是"断开连接"继续跑,而 await 是"挂起等待"并能恢复连续执行上下文。
为什么 async/await 不会导致线程爆炸
传统 GCD 的一个隐患:当你用 semaphore.wait() 或 DispatchGroup.wait() 真正"等"结果时,线程被阻塞(占着资源啥也不干)。系统发现线程不够用时会持续创建新线程,最终导致线程爆炸。
// ❌ 危险:阻塞线程(传统方式的隐患)
let sema = DispatchSemaphore(value: 0)
fetchData { data in sema.signal() }
sema.wait() // 线程在此阻塞,占着系统资源却无法被复用
// 并发请求多时,可能导致线程数量爆炸
Swift Concurrency 的协作式线程池解决了这个问题:
// ✅ 协作式挂起:线程释放回池子,完全不阻塞
let data = await fetchData()
// 线程池始终维持在约等于 CPU 核数的小规模,高效复用
Swift Concurrency 的线程池设计
传统 GCD 线程池(可能爆炸) Swift Concurrency 协作式线程池
┌──────────────────────────┐ ┌──────────────────────────┐
│ 线程1(等待网络,阻塞) │ │ 线程1(执行 Task A) │
│ 线程2(等待数据库,阻塞) │ │ 线程2(执行 Task B) │
│ 线程3(等待文件,阻塞) │ vs │ 线程3(执行 Task C) │
│ 线程4(新建中...) │ │ ← 线程数 ≈ CPU 核数 │
│ 线程N(继续新建...) 💥 │ │ Task 挂起时释放线程,不占用 │
└──────────────────────────┘ └──────────────────────────┘
一句话总结:
-
传统回调:调用线程不等待,但回调上下文断开,线程安全靠自己保证,用
wait() 等待时会阻塞线程
-
async/await:挂起点释放线程,调度器恢复时还原上下文,Actor 保证线程安全,系统始终保持小规模线程池
这就是为什么同样是"异步",Swift Concurrency 在高并发场景下比传统 GCD 回调效率更高、更安全。
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()
}
}
}
深入层:设计思想解析
结构化并发的思想来源
结构化并发的核心理念来自结构化编程的类比:就像 if/for/while 让控制流有明确的进入和退出点,结构化并发让并发任务的生命周期也有明确的边界。
// 传统 GCD:任务生命周期不受控
func fetchData() {
DispatchQueue.global().async {
// 这个任务完全脱离 fetchData 的控制
// fetchData 返回后,任务仍在跑
}
}
// 结构化并发:任务生命周期受作用域约束
func fetchData() async {
async let result = networkCall() // 任务在这里创建
let data = await result // 函数返回前,任务必须完成
} // ← 离开作用域,所有子任务保证已结束
三大核心约束
| 约束 |
含义 |
| 父子关系 |
子任务归属于父任务,父任务取消时子任务自动取消 |
| 生命周期包含 |
父任务不能在子任务完成之前结束 |
| 错误传播 |
子任务的错误必须传递给父任务处理 |
非结构化 vs 结构化对比
// ❌ 非结构化(Task.detached)—— 孤儿任务,生命周期不受控
Task.detached {
await riskyOperation() // 即使调用方已取消,这里仍然在跑
}
// ✅ 结构化(async let / TaskGroup)—— 任务有明确的父子关系
await withTaskGroup(of: String.self) { group in
group.addTask { await fetch("A") }
group.addTask { await fetch("B") }
// 离开 withTaskGroup 之前,所有子任务保证结束
}
这套思想由 Nathaniel J. Smith 的 Notes on structured concurrency 奠基,Swift 从 5.5 开始通过 async let、TaskGroup、actor 全面落地。与 Kotlin 协程的 StructuredConcurrency 一脉相承,但 Swift 通过编译器强制实施,更难写错。
结构化并发:任务树模型
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),提前消灭隐患
八、参考资源
九、本期互动
小作业
基于本文的 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 期:Actor 可重入设计深析 · ○ 第 3 期:Swift 6 严格并发检查实战 · ○ 第 4 期:待定