iOS Swift 并发与任务取消
iOS Swift 并发与任务取消
摘要:Swift 的
async/await、Task 和 Actor 让异步代码更容易阅读,也提供了结构化并发的生命周期约束。本文从异步函数、并发子任务、主线程隔离到取消处理,说明如何写出可控的 iOS 并发代码。
一、异步函数表达等待点
async 标记一个可能挂起的函数,await 标记调用方可能暂停的位置。挂起不会阻塞当前线程:
struct Article: Decodable {
let id: Int
let title: String
}
func loadArticles(using service: ArticleService) async throws -> [Article] {
try await service.fetchArticles()
}
调用异步函数的上下文也需要支持 await。错误可通过 throws 向上传递,调用方使用 do/catch 决定如何呈现。
二、并发执行独立工作
若两个请求互不依赖,可使用任务组并发启动并聚合结果:
func loadDashboard() async throws -> Dashboard {
async let profile = service.fetchProfile()
async let summary = service.fetchSummary()
return try await Dashboard(profile: profile, summary: summary)
}
async let 创建结构化子任务,父任务离开作用域时会等待子任务完成,并遵循取消传播。不要为了并发而并发:若任务依赖前一步结果,仍需按顺序执行。
对动态数量的同类工作可使用 withTaskGroup 或 withThrowingTaskGroup,并控制并发数,避免一次发起过多网络请求。
三、把 UI 更新隔离到主执行器
SwiftUI 模型或 UIKit 状态通常应在主执行器更新。使用 @MainActor 约束相关类型或方法:
@MainActor
final class ArticleModel: ObservableObject {
@Published private(set) var articles: [Article] = []
@Published private(set) var message: String?
func reload() async {
do {
articles = try await service.fetchArticles()
message = nil
} catch is CancellationError {
return
} catch {
message = "暂时无法加载内容"
}
}
}
异步函数挂起期间,执行器可以运行其他任务;它恢复时仍需遵守隔离规则。不要用 DispatchQueue.main.async 掩盖并发模型不清的问题。
四、取消是协作式的
调用 Task.cancel() 只会发出取消信号,不会强行终止任意代码。任务需要在合适位置响应取消:
func process(_ items: [Item]) async throws -> [ResultItem] {
var output: [ResultItem] = []
for item in items {
try Task.checkCancellation()
output.append(try await transform(item))
}
return output
}
网络框架通常会将任务取消映射到底层请求取消。CPU 密集循环则应定期检查取消。取消不是普通业务失败,不应显示成“服务器错误”。
五、界面生命周期与任务
SwiftUI 的 .task 会将任务与视图生命周期关联,视图消失时可以取消;依据输入值变化重启任务时,应确保旧任务取消后不会覆盖新结果。UIKit 可由控制器保存 Task 句柄,并在生命周期结束或请求被替代时取消。
对于搜索输入,可以对值去重、延迟处理,并在新查询到来时取消旧查询。仅仅取消旧任务仍不够;若底层服务不响应取消,可增加请求序号,只接受最新请求的结果。
六、用 Actor 保护共享可变数据
多个任务并发访问同一可变缓存时,actor 提供隔离边界:
actor ArticleCache {
private var values: [Int: Article] = [:]
func value(for id: Int) -> Article? {
values[id]
}
func insert(_ article: Article) {
values[article.id] = article
}
}
跨 Actor 调用通常需要 await。Actor 保护内部状态免于数据竞争,但不要在 Actor 内长时间等待无关工作,否则会让需要访问该 Actor 的任务排队。
七、常见问题
- 不要用
Task.detached作为普通异步调用的替代,它脱离父任务上下文和生命周期。 - 不要捕获控制器形成强引用循环;明确任务的所有者和取消时机。
- 不要假设
async一定在后台线程运行;异步描述的是挂起能力,不是线程承诺。 - 不要忽略并发访问警告;它往往暴露真正的数据竞争。
- 错误、取消与空结果应分开建模。
八、测试异步代码
异步单元测试可直接声明为 async,使用可控的假服务验证成功、失败和取消情形。避免依赖固定 sleep 时间;由测试主动释放延迟点或提供确定输入,测试会更稳定。
九、总结
Swift 并发的关键是让任务有明确的父子关系、共享数据有隔离边界、取消能传播到实际工作,并让 UI 状态回到主执行器更新。把这些规则应用到请求、搜索和后台处理后,异步代码既易读,也更容易测试。
- 点赞
- 收藏
- 关注作者
评论(0)