Swift并发场景下Task与AsyncStream结合引发循环引用导致deinit未执行的问题咨询
这是个非常典型的Swift并发循环引用场景,咱们一步步拆解背后的逻辑:
一、Playground中两种情况的差异原因
1. FailedDeinit无法触发deinit的核心问题
在FailedDeinit里,init中创建的Task闭包调用了self.subscribe()——这里的闭包会隐式强引用self,因为要调用实例方法。同时self的task属性又持有这个Task,形成了一个闭环:self → task → self → task...
更关键的是,subscribe方法里的for await p in stream会一直挂起(因为这个AsyncStream没有发送任何元素也没有终止),导致Task永远不会完成。这个未完成的Task会一直持有self,循环引用无法打破,所以deinit永远不会被调用。
2. SuccessDeinit能正常释放的原因
而SuccessDeinit的Task闭包内部完全没有引用self——既没调用实例方法,也没访问实例属性。这意味着Task并没有持有self,唯一的引用是self持有Task。当你把instance置为nil时,self的引用计数降为0,deinit正常执行,task属性也随之被释放(虽然Task本身可能还在后台挂起,但已经没有循环引用了)。
二、SwiftUI中添加[weak self]仍无效的原因
你尝试在Task闭包中添加[weak self],看起来是想打破循环,但这里有个容易忽略的点:
当Task执行await self?.subscribe()时,如果self此时还存在,调用subscribe方法的瞬间会临时强引用self——因为实例方法的执行依赖于self的存在。而subscribe里的for await循环是无限挂起的,这个临时强引用就变成了永久强引用,循环引用依然存在:self → task → (subscribe执行期间的强引用)self → task...
只要subscribe的循环不结束,self就会一直被持有,无法触发deinit。
三、解决方案:主动打破循环引用
要解决这个问题,核心是让Task能被终止,从而结束subscribe里的循环,打破强引用链。最直接的方式是在deinit中取消Task:
class FixedDeinit { init() { print(#function, id) task = Task { await subscribe() } } deinit { print(#function, id) task?.cancel() // 在销毁时取消Task } func subscribe() async { // 创建Stream时监听终止事件 let stream = AsyncStream<Double> { continuation in continuation.onTermination = { reason in print("Stream terminated: \(reason)") } } do { // 使用try await捕获取消错误 for try await p in stream { print("\(p)") } } catch { print("Subscribe cancelled: \(error)") } } private var task: Task<(), Swift.Error>? let id = UUID() }
当instance被置为nil时,deinit会被调用,此时取消Task:
Task的取消会触发AsyncStream的终止逻辑for try await循环会抛出CancellationError,跳出循环,subscribe方法执行完毕Task完成,不再持有self,循环引用彻底打破
总结
这类问题的本质是未完成的Task + 隐式强引用形成的循环。只要确保Task能在实例销毁时被取消,让异步任务正常结束,就能打破循环引用,触发deinit。
内容的提问来源于stack exchange,提问作者Steve Begin

