4、任务(Task)基础:Task类型、创建异步任务、任务优先级、任务取消机制
好,咱们今天来聊聊 Swift 并发里最基础、也最核心的东西——Task。
说实话,我刚接触 Swift 并发那会儿,第一反应是:这不就是 GCD 的升级版吗?用着用着才发现,差远了。Task 不仅仅是「开个后台线程干活」,它背后有一套完整的任务管理机制。你想想看,一个异步任务从创建到取消,中间涉及的东西其实挺多的。
我个人习惯把 Task 理解成「Swift 并发世界里的基本执行单元」。就像函数是同步世界的基本单元一样,Task 就是异步世界里的那个「最小工作包」。
Task 类型:结构化 vs 非结构化
Swift 里的 Task 分两种:结构化任务和非结构化任务。这俩概念刚接触时容易懵,我当年也绕了一阵子。
结构化任务,说白了就是有爹有妈的任务。它必须在一个已有的异步上下文中创建,并且它的生命周期和父任务绑定在一起。父任务取消了,子任务也跟着取消。父任务等子任务执行完,自己才能结束。
举个例子:
func fetchUserData() async -> User {
// 这里是在异步上下文中
let profile = await fetchProfile() // 子任务
let settings = await fetchSettings() // 子任务
return User(profile: profile, settings: settings)
}
这种写法就是结构化并发。fetchProfile 和 fetchSettings 的生命周期完全由 fetchUserData 管理。父任务没结束,子任务跑不完?那就等着。
非结构化任务就不一样了。它用 Task.init 或 Task.detached 创建,不依赖任何父任务。说白了就是「我自己想跑就跑,谁也管不着」。
Task {
// 非结构化任务
let result = await someAsyncFunction()
print(result)
}
我在项目中遇到过一个问题:用 Task.detached 创建了一个后台下载任务,结果用户退出页面后任务还在跑,白白浪费流量。后来我改成结构化任务,配合父任务的取消机制,问题就解决了。
创建异步任务:几种常见姿势
创建 Task 的方式其实就那么几种,但每种都有自己的适用场景。
| 创建方式 | 说明 | 典型场景 |
|---|---|---|
| Task { } | 创建非结构化任务,继承当前 Actor 的上下文 | 在同步函数中启动异步操作 |
| Task.detached { } | 创建独立任务,不继承上下文 | 后台计算、不关心调用者状态的任务 |
| async let | 创建结构化子任务 | 并发执行多个独立异步操作 |
| TaskGroup | 动态创建一组结构化子任务 | 不确定数量的并发任务 |
我个人最常用的是 async let 和 TaskGroup。为什么呢?因为它们自带结构化特性,生命周期清晰,不容易出幺蛾子。
// async let 的典型用法
async let user = fetchUser()
async let posts = fetchPosts()
let (loadedUser, loadedPosts) = await (user, posts)
你看,两行代码就搞定了并发请求。而且如果父任务被取消了,这两个子任务也会自动取消。省心。
任务优先级:别什么都往高优先级塞
任务优先级这个东西,说白了就是告诉系统:「这个任务比较重要,你先处理它」。
Swift 提供了几个优先级等级:
- high:用户交互相关的任务,比如 UI 更新
- default:默认优先级,大部分任务用这个就行
- low:后台预加载、日志上传等不紧急的任务
- background:最低优先级,比如数据同步、备份
Task(priority: .high) {
await updateUI()
}
Task(priority: .background) {
await syncDataToServer()
}
我曾经犯过一个错误:把所有网络请求都设成 high 优先级。结果呢?用户滑动列表时卡得要命,因为 UI 更新和网络请求在抢资源。后来我学乖了,只有用户「正在看」的内容才用 high,预加载的数据一律 low 或 background。
任务取消机制:优雅地停下来
任务取消,嗯,这是个容易踩坑的地方。
Swift 的 Task 取消是协作式的。什么意思呢?就是说系统不会强行杀掉你的任务,而是给你发一个「取消信号」,你自己决定要不要响应。
func fetchData() async throws -> Data {
// 检查是否被取消
try Task.checkCancellation()
// 或者用更温和的方式
if Task.isCancelled {
// 做一些清理工作
return defaultData
}
// 继续执行...
}
我个人习惯在长时间运行的任务里,每隔一段逻辑就检查一下取消状态。比如下载大文件时,每下载 1MB 就检查一次:
func downloadLargeFile() async throws {
for chunk in fileChunks {
// 每次处理一个块之前检查
try Task.checkCancellation()
// 下载这个块
await downloadChunk(chunk)
}
}
如果不检查取消状态,任务会一直跑到完。即使用户取消了操作,后台还在傻乎乎地干活。浪费资源不说,还可能引发奇怪的问题。
还有一个细节:withTaskCancellationHandler。这个函数可以让你注册一个取消时的回调,适合做一些清理工作:
func performTask() async {
await withTaskCancellationHandler {
// 主要任务逻辑
for i in 0..<100 {
try? await Task.sleep(nanoseconds: 1_000_000_000)
print("Working: \(i)")
}
} onCancel: {
// 取消时的清理工作
print("Task was cancelled, cleaning up...")
}
}
这个我在项目里用过一次:用户取消图片上传时,需要删除已经上传到服务器的部分文件。onCancel 回调里正好做这个事。
好了,Task 的基础知识就这些。总结一下:
- 结构化任务有父任务管理,生命周期清晰
- 非结构化任务自由度高,但要自己负责生命周期
- 优先级别滥用,高优先级留给用户交互
- 取消是协作式的,记得主动检查
下一章咱们聊聊 Actor,那玩意儿才是 Swift 并发里真正有意思的东西。