5.2 结构化同台:async-await与前后台


5.2 结构化同台:async-await 与前后台

本节摘要:Swift 5.5 的 async/await 把回调式并发写成顺序代码:async 函数声明"我会挂起",await 标记挂起点,Task 承载与取消任务,MainActor 保证回主舞台刷新。本节以"下拉刷新"和"界面退场取消任务"两个场景,讲清这套新语法的生命周期语义。结论先行:await 是"让出主舞台的暂停点",Task 的 cancel 要写进界面退场的收尾清单。

队列语言(5.1 节)解决了"活往哪送",但回调链把一段逻辑切成碎片散落各处。这一节换新台词本——把碎片重新缝成顺序的形状。

新一代台词本:从回调到 await

先用同一个需求对比两代写法。需求:取场次(耗时)→ 刷新界面。回调版:

// 回调版:逻辑被切成两段,错误处理散在闭包里 func loadBoardCallback(completion: @escaping ([String]?) -> Void) { DispatchQueue.global().async { Thread.sleep(forTimeInterval: 0.2) // 模拟网络 completion(["雾中灯塔 19:00", "星际列车 20:30"]) } } loadBoardCallback { board in DispatchQueue.main.async { print("刷新:\(board ?? []) 条") // 输出:刷新:2 条 } }

async/await 版:

// async 版:声明"此函数会挂起",调用方在 await 处暂停等结果 func fetchBoard() async -> [String] { Thread.sleep(forTimeInterval: 0.2) // 教学用;真实代码用异步休眠 return ["雾中灯塔 19:00", "星际列车 20:30"] } @MainActor func refreshBoard() async { // MainActor:整个函数钉在主舞台 let board = await fetchBoard() // 挂起点:让出主舞台,结果回来再继续 print("刷新:\(board.count) 条") // 输出:刷新:2 条 }

第二版按人类阅读顺序写:取数、刷新,一行一件事。关键认识:await 不是开线程,而是标记"此处可暂停,暂停期间主舞台照常营业"。挂起期间系统线程去跑别的任务,结果就绪后从暂停点恢复——主线程从未被阻塞。

Task:把 async 函数推上舞台

async 函数不能在同步上下文里直接调用,要有 Task 这个"启动器":

// 同步世界里点火一个异步任务 Task { await refreshBoard() } // 带优先级与延迟的任务 Task(priority: .userInitiated) { print("高优先级任务派出") } // 结构化并发:async let 并行取两份数据,隐式等齐 func loadAll() async -> (Int, Int) { async let showtimes = fetchCount(kind: "场次") // 并行分支一 async let news = fetchCount(kind: "影讯") // 并行分支二 return await (showtimes, news) // 等两个分支都回来 } func fetchCount(kind: String) async -> Int { Thread.sleep(forTimeInterval: 0.1) print("\(kind)接口返回") return kind == "场次" ? 2 : 1 }

对比 5.1 节的 DispatchGroup 案例:async let 两个词替代了 enter/leave/notify 三件套。这就是"结构化并发"的含义——子任务的生命周期被语法结构圈定在父作用域里,父函数返回前子任务必然收束,不像散养的闭包会逃逸到天边(4.1 节泄漏案的温床)。

生命周期主战场:界面退场时取消任务

这是本节与视图生命周期剧场的交点。场景:排片界面发起加载,用户等不及返回了上一页——此时任务该被取消,否则它完成后会尝试刷新一个已经谢幕的界面(4.2 节 weak 挡住的是崩溃,但浪费的流量与电量仍要靠取消来省)。

界面退场与任务取消的时序

代码落地,三步走:发起时持柄、退场时取消、任务内响应取消:

@MainActor final class BoardLoader { private var task: Task<Void, Never>? // 任务句柄:界面退场的抓手 func appear() async { task = Task { // 发起并记下句柄 await runRefresh() } } func disappear() { // 退场收尾 task?.cancel() // 取消进行中的任务 task = nil print("界面退场,任务已取消") } private func runRefresh() async { let board = await fetchBoard() if Task.isCancelled { // 响应取消:结果回来先验生死 print("已取消,丢弃结果") // 输出(退场情形):已取消,丢弃结果 return } print("刷新:\(board.count) 条") } }

两个细节值得放大。其一,cancel() 只是"举旗"——它不杀线程,任务内的代码要在合适的检查点(Task.isCancelledtry await 的抛错路径)主动看到旗子并收工;纯计算的循环要靠自己定期检查。其二,取消之后 fetchBoard 的结果仍可能回来(网络已发出),所以结果回来后的第一件事是验取消——顺序不能反。

⚠️ 常见坑:以为 cancel 后闭包/任务就立刻不跑了。取消是协作式的,长循环里不写检查点,任务会跑完全程才罢休;另一个极端是界面退场时忘了 cancel,只靠 weak 兜底——不崩溃但白耗资源,列表页滚动几轮就是几十个僵尸任务。

完整案例:可取消的余票轮询

背景:排片页每两秒轮询余票,退场即停。操作:把"循环 + 检查取消 + 休眠"组装成一个自觉的任务:

func pollSeats() async { var tick = 0 while !Task.isCancelled { // 每轮先看旗子 tick += 1 print("第 \(tick) 轮:余票 \(42 - tick)") // 模拟余票递减 do { try await Task.sleep(nanoseconds: 300_000_000) // 异步休眠 0.3 秒(可中断) } catch { print("休眠被取消,轮询收工") // cancel 会中断 sleep 并抛错 return } } print("检测到取消,正常收工") } let pollTask = Task { await pollSeats() } Thread.sleep(forTimeInterval: 1) // 让它跑几轮(模拟用户停留) pollTask.cancel() // 模拟界面退场 Thread.sleep(forTimeInterval: 0.2) print("演示结束") // 输出(节选): // 第 1 轮:余票 41 // 第 2 轮:余票 40 // 第 3 轮:余票 39 // 休眠被取消,轮询收工 // 演示结束

结果解读:轮询跑了三轮后被 cancel,Task.sleep 被打断并抛错,catch 里收工退出——这就是"协作式取消"的标准闭环。变式:把 while 条件改成死循环、去掉 catch,任务会无视取消跑满全程;再变式:两个轮询任务放进同一个 TaskGroup,取消父任务时整组一起收工——结构化并发的又一红利。

前后台对照时间线

界面在场期间,任务的一生

界面在场期间,任务的一生

💡 关键直觉:GCD 想"往哪送",async/await 想"谁等谁"。新语法的真正礼物不是少写几个字符,而是生命周期被结构圈定——父作用域等子任务、退场即可取消,第4章追过的"逃逸闭包失控"问题在这里从语法层面收窄。

收场白

  • async 声明挂起可能,await 标记挂起点,主舞台在暂停期间照常营业;
  • Task 是启动器与句柄,退场时对着句柄 cancel 是界面收尾的标准动作;
  • 取消是协作式:任务内用 isCancelled 与可中断 sleep 配合收工;
  • async let 并行分支、TaskGroup 整组收束——结构化并发圈定子任务生命周期;
  • @MainActor 把状态修改钉在主舞台,接替手写 main.async。

同台演出的纪律立好了。下一幕展示舞台机关:泛型、不透明类型、属性包装器与结果构建器——它们让你写一次机关,所有剧目复用。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U