【问题标题】:Issues with retain cycle using AsyncStream in a Task在任务中使用 AsyncStream 的保留周期问题
【发布时间】:2022-06-11 12:27:32
【问题描述】:

在使用新的 Swift 并发工具时发现了这个问题。

设置如下:

class FailedDeinit {
    
    init() {
        print(#function, id)
        task = Task {
            await subscribe()
        }
    }
    
    deinit {
        print(#function, id)
    }
    
    func subscribe() async {
        let stream = AsyncStream<Double> { _ in }
        for await p in stream {
            print("\(p)")
        }
    }
    
    private var task: Task<(), Swift.Error>?
    let id = UUID()
}

var instance: FailedDeinit? = FailedDeinit()
instance = nil

在 Playground 中运行此代码会产生以下结果:

init() F007863C-9187-4591-A4F4-BC6BC990A935

!!! deinit 方法永远不会被调用!!!

奇怪的是,当我把代码改成这样的时候:

class SuccessDeinit {
    
    init() {
        print(#function, id)
        task = Task {
            let stream = AsyncStream<Double> { _ in }
            for await p in stream {
                print("\(p)")
            }
        }
    }
    
    deinit {
        print(#function, id)
    }
    
    private var task: Task<(), Swift.Error>?
    let id = UUID()
}

var instance: SuccessDeinit? = SuccessDeinit()
instance = nil

通过直接在Task中移动subscribe()方法中的代码,控制台中的结果变为:

init() 0C455201-89AE-4D7A-90F8-D6B2D93493B1
deinit 0C455201-89AE-4D7A-90F8-D6B2D93493B1

这可能是一个错误,但肯定有一些我不明白的地方。我欢迎任何有关这方面的见解。

~!~!~!~!

这很疯狂(或者我可能是?),但使用 SwiftUI macOS 项目。我仍然没有和你一样的行为。查看我保留 FailedDeinit 和 SuccessDeinit 类的相同定义但在 SwiftUI 视图中使用它们的代码。

struct ContentView: View {
    @State private var failed: FailedDeinit?
    @State private var success: SuccessDeinit?
    var body: some View {
        VStack {
            HStack {
                Button("Add failed") { failed = .init() }
                Button("Remove failed") { failed = nil }
            }
            HStack {
                Button("Add Success") { success = .init() }
                Button("Remove Success") { success = nil }
            }
        }
    }
}


class FailedDeinit {
    
    init() {
        print(#function, id)
        task = Task { [weak self] in
            await self?.subscribe()
        }
    }
    
    deinit {
        print(#function, id)
    }
    
    func subscribe() async {
        let stream = AsyncStream<Double> { _ in }
        for await p in stream {
            print("\(p)")
        }
    }
    
    private var task: Task<(), Swift.Error>?
    let id = UUID()
}

【问题讨论】:

  • 这听起来很有趣,但请在真正的应用程序中进行测试,而不是在 Playground 中进行,因为 Playground 无法正确模拟内存管理(或异步/等待,就此而言)。
  • 我第一次在真正的 macOS 应用程序中工作时偶然发现了这个问题,但试图在这种环境中找到问题的解决方案并不实际。
  • 但是突然发现这一切都发生在 SwiftUI 项目的 State 变量中,你已经完全转移了目标。这不公平。我回答了你实际提出的问题。没有问出你真正想知道答案的问题是你自己做的。
  • 哦,天哪……我不是故意冒犯或类似的。只是有时,提出问题并不像看起来那么容易。老实说,真正的问题出现在一个没有什么特别之处且不涉及 SwiftUI 的类中。该课程和应用程序的其余部分非常复杂,我试图通过在操场上工作来隔离问题,由于结果是相同的,我从未怀疑过操场。在那之后,我仍然想保持问题隔离,我构建了一个小型 SwiftUI 应用程序来测试你的想法,只是报告说问题仍然没有解决。

标签: swift async-await concurrency task asyncstream


【解决方案1】:

这与 async/await 或 AsyncStream 没有任何关系。这是一个完全正常的保留周期。你(FailedDeinit 实例)保留了任务,但任务引用了subscribe,这是你的一个方法,即self,所以任务保留了你。因此,只需像打破任何其他保留周期一样打破保留周期。换个方式

    task = Task {
        await subscribe()
    }

到

    task = Task { [weak self] in
        await self?.subscribe()
    }

另外,请务必在真实项目中进行测试,而不是在操场上进行测试,因为操场在这方面并不能说明任何事情。这是我使用的代码:

import UIKit

class FailedDeinit {

    init() {
        print(#function, id)
        task = Task { [weak self] in
            await self?.subscribe()
        }
    }

    deinit {
        print(#function, id)
    }

    func subscribe() async {
        let stream = AsyncStream<Double> { _ in }
        for await p in stream {
            print("\(p)")
        }
    }

    private var task: Task<(), Swift.Error>?
    let id = UUID()
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        var instance: FailedDeinit? = FailedDeinit()
        instance = nil
   }
}

【讨论】:

  • 感谢您的回答!我刚刚尝试了您的提议,并且(不幸的是)它并没有改变结果。 deinit 仍未被调用。
  • 在实际项目中尝试,而不是在操场上。那是我测试的地方。在这方面,游乐场并不表示任何东西。 — 我将添加我使用的实际代码。
【解决方案2】:

考虑以下几点:

task = Task {
    await subscribe()
}

确实引入了对self 的强引用。您可以通过以下方式解决该强引用:

task = Task { [weak self] in
    await self?.subscribe()
}

但这只是问题的一部分。

问题是,一旦subscribe 开始执行,尽管闭包中有weak 引用,它仍将保持对self 的强引用,直到subscribe 完成。所以,这个weak 参考是谨慎的,但它不是全部。

这里的问题比乍看之下更微妙。考虑以下几点:

func subscribe() async {
    let stream = AsyncStream<Double> { _ in }
    for await p in stream {
        print("\(p)")
    }
}

subscribe 方法将继续执行,直到流调用 finish。但是你永远不会finishstream。 (你也没有yield 任何值。哈哈。)无论如何,如果AsyncStream 中没有任何内容,一旦subscribe 启动,它将永远不会返回,因此永远不会释放self。

因此,让我们考虑您的第二个演绎版,当您创建 Task 时,绕过 subscribe:

task = Task {
    let stream = AsyncStream<Double> { _ in }
    for await p in stream {
        print("\(p)")
    }
}

是的,您会看到对象被释放,但是您忽略了Task 也永远不会完成!所以,不要仅仅因为包含的对象被释放就陷入一种虚假的安全感:Task 永远不会结束!

这一切都可以通过将流更改为实际产生值并最终 finish 来说明:

task = Task {
    let stream = AsyncStream<Double> { continuation in
        Task {
            for i in 0 ..< 10 {
                try await Task.sleep(nanoseconds: 1 * NSEC_PER_SECOND)
                continuation.yield(Double(i))
            }
            continuation.finish()
        }
    }

    for await p in stream {
        print("\(p)")
    }
}

在这种情况下,如果您在流进行时将其关闭,您将看到AsyncStream 会一直持续到结束。 (而且,如果您碰巧在方法中执行此操作,则相关对象也将被保留,直到任务被取消。)

所以,如果您希望 AsyncStream 完成,您需要做的是 cancel Task。

现在,在我的示例中,我使用Task.sleep。如果取消Task,则会引发错误。所以,如果我cancel 在释放视图控制器(或其他任何东西)时这样做,那么产生值 0 到 9 的示例将停止并且任务将被释放。但是,如果您在 AsyncStream 中的异步进程不处理取消(顺便说一句,他们总是应该),那么您需要在 AsyncStream 中添加一个 Task.isCancelled 到 finish 流。

这一切都取决于您的AsyncStream 真正在做什么。但是在简化 MCVE 和删除AsyncStream 的内容的过程中,您同时不处理取消并且永远不会调用finish。这两者结合起来,就体现了您所描述的问题。

【讨论】:

    猜你喜欢
    • 2020-09-27
    • 1970-01-01
    • 2012-09-29
    • 1970-01-01
    • 2018-05-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-04-02
    相关资源
    最近更新 更多