Swift的并发模型在Swift 5.5中经历了一次范式转变,引入了结构化并发以取代传统的Grand Central Dispatch模式,这些模式通常导致任务孤立和资源泄漏。在此之前,开发人员手动管理DispatchGroup实例来跟踪并发工作,需要显式同步以防止在取消过程中发生竞争条件。TaskGroup抽象被设计为本地封装父子关系树,确保运行时维护生命周期元数据,而不是开发人员。
核心问题在于保持确定性层次结构,其中父任务可以可靠地向所有后代信号取消,而无需遍历全局注册表或手动弱引用数组。使用OperationQueue的传统方法要求显式注册和注销完成处理程序,创建脆弱的状态管理,如果由于早期退出而跳过了完成处理程序,则会失败。此外,传播取消需要复杂的原子标志轮询,通常导致响应延迟或过高的CPU开销。
Swift通过在每个任务的上下文中嵌入一个任务记录,该记录指向其父任务,从而解决了这个问题,形成一个以TaskGroup为根的侵入性链表。当调用addTask时,运行时在此链表中插入一个子任务记录,原子地将其注册到父任务的取消处理程序中。取消机制利用状态机:当调用cancelAll()时,运行时遍历该链表,在每个子任务的元数据上设置isCancelled标志,并唤醒挂起的执行器。这确保了O(n)的传播,其中n是树的深度,避免了全局锁。
import Foundation func downloadImages(urls: [URL]) async throws -> [Data] { try await withThrowingTaskGroup(of: Data.self) { group in for url in urls { group.addTask { // 子任务自动检查父任务取消 let (data, _) = try await URLSession.shared.data(from: url) return data } } // 模拟用户取消 group.cancelAll() var results: [Data] = [] for try await data in group { results.append(data) } return results } }
一个媒体处理应用需要为10,000张图像生成缩略图,同时允许用户在处理中途取消。工程团队最初使用DispatchGroup方法,线程安全地在NSHashTable中跟踪活动的URLSessionDataTask对象,以便实现取消。
第一个解决方案使用了带有DispatchSemaphore的DispatchGroup来限制并发性。虽然可行,但需要复杂的逻辑来从取消集移除已完成的任务。在取消信号和集合枚举之间,任务完成导致竞争条件,从而导致应用引用已释放的对象。该方法还会在视图控制器被取消时泄漏内存,因为DispatchGroup通知强引用了委托。
第二个方法采用了Combine的FlatMap和PassthroughSubject进行取消。这提供了更好的组合性,但带来了来自发布者链分配的显著内存开销。取消传播需要在集合中存储AnyCancellable令牌,需要手动清理。声明式抽象隐藏了实际的任务层次结构,当取消信号未能通过操作链传播时,使调试变得困难。
团队迁移到Swift的TaskGroup。这消除了手动NSHashTable管理,因为运行时自动将每个缩略图生成任务链接到组的取消域。当用户点击取消时,视图控制器调用group.cancelAll(),原子地向所有正在运行的任务发出信号,以便在下一个await挂起点停止。这种解决方案确保在视图释放后没有孤立的任务继续处理,withThrowingTaskGroup的确定性范围确保即使函数抛出错误也能自动清理。
取消延迟从平均500毫秒(等待手动集合枚举)下降到10毫秒以下(直接链表遍历)。内存分析显示,在取消后没有泄漏的Task对象,代码库在同步样板中减少了40行。
TaskGroup如何处理子任务忽略取消而继续无限执行的情况?
候选人常常认为TaskGroup强制终止任务或注入异常。实际上,Swift的取消是合作的:运行时在任务的上下文中设置isCancelled标志,但任务会继续,直到它击中挂起点或显式检查Task.isCancelled。子任务必须定期轮询Task.checkCancellation()或依赖于支持取消的API。如果任务执行一个没有挂起点的紧凑CPU束缚循环,它会无限期阻塞组的完成。为避免这种情况,长时间运行的计算应使用Task.yield()或将工作拆分成检查取消标志的块。
为什么在调用cancelAll()后添加任务到TaskGroup仍会导致该新任务立即取消?
许多人认为cancelAll()是仅发送给现有子任务的一次性信号。然而,Swift的实现将TaskGroup本身标记为在其状态记录中被取消。当之后调用addTask时,运行时在任务创建期间原子地检查组的取消状态;如果被取消,则新子任务的isCancelled标志预设。这确保了后添加的任务无法逃脱取消域,保持被取消的范围不能产生新的有效结果的结构保证。这防止了在取消逐步结束期间添加的任务滑过的竞争条件。
TaskGroup的结构化并发与通过Task.init创建的Task在捕获变量的内存管理上有何根本区别?
候选人常常忽视TaskGroup子任务继承父上下文的actor隔离和优先级,但更关键的是,它们仅在组范围退出之前扩展捕获变量的生命周期。相比之下,使用Task { ... }创建的非结构化Task对象在创建范围的生命周期之外持续存在,可能无限期捕获self。这意味着在TaskGroup中,如果在addTask中捕获self,则不需要[weak self],因为任务无法超出withThrowingTaskGroup块的生命期。然而,开发人员往往错误地应用来自非结构化任务的[weak self]模式,从而不必要地复杂化代码,并可能在依赖self存在完成时引入空引用错误。