Swift 并发中的即时任务(Immediate Tasks)详解
即时任务(Immediate Tasks)是 Swift 6.2 采纳 SE-472 后新增的特性,用于解决创建和调度任务时产生初始延迟的问题。
即时任务在 Swift 并发中是 Swift 6.2 采纳 SE-472 后的新特性。它们适用于你希望避免因创建和调度任务而产生初始延迟的场景。当任务的工作量极小,或者已知当前处于正确的 actor 上但尚未进入异步上下文时,你可能需要用到它。
在深入细节之前,建议你仅在真正确定需要时才使用这一特性。立即启动任务以尽快获得结果听起来很诱人,但在大多数情况下,依赖标准的并发执行调度完全没有问题。下面我们来了解具体细节。
立即启动任务
即时任务允许你创建一个在调用者执行上下文中同步运行的任务。普通的 Task 会被创建并调度到稍后运行,而即时任务会立即开始执行 ,直到遇到第一个真正的挂起点。
这看起来差异不大,但在性能敏感的代码或已在正确 actor 上运行的同步 API 中,这种差异可能至关重要。
你可以使用 Task.immediate 创建即时任务:
func startLoading() {
Task.immediate {
await loadInitialData()
}
}
该任务在调用者的执行器上同步开始执行。这意味着任务的第一部分会在 startLoading() 继续执行之前运行,直到第一个真正挂起的点。
这与普通任务不同:
func startLoading() {
Task {
await loadInitialData()
}
print("这行可能在 loadInitialData 启动之前运行")
}
使用普通 Task 时,Swift 会将操作调度到稍后运行。在很多情况下,这正是你想要的——它避免了阻塞调用者,并给运行时提供了高效调度工作的空间。
然而,在某些情况下,这个额外的调度步骤并不理想。如果工作量很小、可能提前返回,或者需要在调用者继续之前更新 actor 隔离的状态,Task.immediate 是更好的选择。
运行至第一个挂起点
最重要的规则是:即时任务只会同步运行到真正挂起为止。await 关键字标记的是潜在的挂起点,但并非每个 await 在实际运行时都会挂起。
func configureView() {
Task.immediate {
guard shouldShowPlaceholder else {
return
}
await updatePlaceholderIfNeeded()
await loadRemoteContent()
}
print("配置继续执行")
}
如果 shouldShowPlaceholder 返回 false,任务会在 configureView() 继续之前完成。如果 updatePlaceholderIfNeeded() 没有挂起,任务会继续同步运行。一旦 loadRemoteContent() 执行了真正的挂起,调用者就可以继续执行,任务则会稍后根据其隔离属性恢复。
换句话说,Task.immediate 提供的是同步启动,而不是保证整个任务同步运行。
从同步 Actor 代码中调用异步代码
当你处于一个已经在正确 actor 上运行的同步函数中时,即时任务就非常有用。这种情况可能来自框架回调或从 @MainActor 代码调用的同步辅助方法。
假设有一个从主 actor 隔离上下文中调用的同步方法:
@MainActor
final class PhotoSelectionModel {
private(set) var selectedPhotoID: UUID?
func selectPhoto(id: UUID) async {
selectedPhotoID = id
await persistSelection(id)
}
private func persistSelection(_ id: UUID) async {
// 将选择存储到数据库或磁盘。
}
}
你无法从同步函数直接调用 selectPhoto(id:),因为它是异步的。普通 Task 可以工作,但它会稍后运行:
@MainActor
func didTapPhoto(id: UUID, model: PhotoSelectionModel) {
Task {
await model.selectPhoto(id: id)
}
// selectedPhotoID 可能还没有更新。
}
如果执行顺序很重要,可以使用即时任务:
@MainActor
func didTapPhoto(id: UUID, model: PhotoSelectionModel) {
Task.immediate {
await model.selectPhoto(id: id)
}
// 任务立即在 MainActor 上开始执行。
}
在这个例子中,任务立即在 MainActor 上开始执行。假设 selectPhoto(id:) 在赋值之前没有挂起,selectedPhotoID 的更新会在调用者继续之前完成。
这是一个微妙但重要的细节。如果 selectPhoto(id:) 在更新 selectedPhotoID 之前就 await 了,调用者可能在状态改变之前就继续执行。
指定特定的 Actor
你也可以在任务闭包中显式指定 actor 隔离:
func handleLegacyCallback(id: UUID, model: PhotoSelectionModel) {
Task.immediate { @MainActor in
await model.selectPhoto(id: id)
}
}
这会要求 Swift 在 MainActor 上运行该任务。如果当前执行器已经匹配 MainActor,任务会立即启动。如果不匹配,Swift 会回退到通常的行为,将工作入队稍后运行。
这使得 Task.immediate 对于通常从主 actor 调用的旧式回调很有用,同时在不在主 actor 上时仍然安全。
注意,Task.immediate { @MainActor in ... } 在你不在 MainActor 上时不会崩溃——它只是像普通 任务一样将任务入队。只有当你真正需要运行时断言且能保证调用者的隔离时,才使用 MainActor.assumeIsolated。
避免执行器阻塞(Overhang)
即时任务最大的风险是执行器阻塞(overhang):在任务挂起之前,在调用者执行器上运行了太多同步工作。
这在主 actor 上尤其危险:
@MainActor
func handleSearchQuery(_ query: String) {
Task.immediate {
let results = expensiveLocalSearch(query)
await updateResults(results)
}
}
如果 expensiveLocalSearch(_:) 耗时 300 毫秒,你的 UI 就会被阻塞 300 毫秒。普通 Task 如果继承了该隔离属性,仍然会在主 actor 上运行,但重要的区别在于 Task.immediate 会在调用者重新获得控制权之前发生阻塞。
建议只在同步的第一部分确定很小时才使用即时任务:
- 更新少量 actor 隔离的状态
- 经过廉价条件检查后提前返回
- 在保持执行顺序的同时从同步上下文启动异步调用
如果工作可能很耗时,应继续使用普通 Task,将工作移出 actor,或者将周围的 API 改为异步。
即时分离任务
Swift 还提供了 Task.immediateDetached,它是 Task.detached 的即时版本。
func cleanupTemporaryFiles() {
Task.immediateDetached(priority: .background) {
await TemporaryFileCleaner.cleanup()
}
}
分离任务课程中的警告同样适用:分离任务不会以相同方式继承调用者的 actor 上下文、任务本地值或取消信号。即时部分只改变任务开始执行的位置,不会让分离任务变为结构化或自动管理的。
实践中,Task.immediateDetached 应该很少用。如果 Task.detached 已经是你的最后手段,即时分离变体就更加特殊了。
任务组中的即时任务
任务组也获得了即时变体:
await withTaskGroup(of: Thumbnail.self) { group in
for photo in photos {
group.addImmediateTask {
await generateThumbnail(for: photo)
}
}
for await thumbnail in group {
await store(thumbnail)
}
}
addImmediateTask 的行为类似于 addTask,但子任务会立即在调用者的执行器上开始,直到第一个真正的挂起点。子任务仍然是组的一部分,因此结构化并发语义(如取消和等待结果)仍然适用。
有一个陷阱需要注意:任务组中的子任务不会自动继承外围上下文的 actor 隔离。此外,如果即时子任务在挂起前执行了大量同步工作,添加任务的循环实际上会变成串行的。
await withTaskGroup(of: Void.self) { group in
for item in items {
group.addImmediateTask {
performExpensiveSynchronousWork(for: item)
}
}
}
上面的例子看起来是并发的,但每个即时子任务可以在下一个任务被添加之前运行其同步工作。如果你想要同步 CPU 工作的并行性,addImmediateTask 可能不是正确的工具。
在第一部分廉价且顺序重要时,使用即时子任务。对于通常的任务组并行性,使用普通 addTask。
总结
即时任务允许你在调用者的执行上下文中同步启动任务。当你需要在保持执行顺序的同时从同步函数执行异步代码时,它们非常有用,尤其是在处理 actor 隔离状态时。关键要点是:Task.immediate 改变的是任务的起始位置,而不是任务的整体生命周期。请谨慎 使用即时任务——它们可能会阻塞调用者执行器直到第一个真正的挂起点,而即时任务组子任务可能意外地使工作变为串行。
原文地址:https://www.avanderlee.com/concurrency/immediate-tasks-in-swift-concurrency-explained/
