【问题标题】:How does retain count with synchronous dispatch work?同步调度的保留计数如何工作?
【发布时间】:2023-04-08 09:19:01
【问题描述】:

我试图解释对象的所有权以及 GCD 是如何工作的。 这些是我学到的东西:

  • 函数将增加其调用对象的保留计数
  • 一个调度块,除非它弱捕获self 会增加计数。
  • 在调度块执行后,它释放捕获的对象,因此self 的保留计数应该减少。但这不是我在这里看到的。这是为什么?
class C {
    var name = "Adam"

    func foo () {
        print("inside func before sync", CFGetRetainCount(self)) // 3
        DispatchQueue.global().sync {
            print("inside func inside sync", CFGetRetainCount(self)) // 4
        }
        sleep(2)
        print("inside func after sync", CFGetRetainCount(self)) // 4 ?????? I thought this would go back to 3
    }
}

用法:

var c: C? = C()
print("before func call", CFGetRetainCount(c)) // 2
c?.foo()
print("after func call", CFGetRetainCount(c)) // 2

【问题讨论】:

  • 保留计数与您的预期不同的原因有很多。比较 stackoverflow.com/questions/4636146/when-to-use-retaincountfriday.com/bbum/2011/12/18/retaincount-is-uselesssdarlington.github.io。 – 在您的情况下,Debug 和 Release 模式的行为是不同的。
  • @MartinR FWIW 我也使用异步块测试了这个,它以非weak 的方式捕获了self,使用了捕获自我weakly 的异步块,或者调用了多个异步块。保留计数的增加/减少,我能够为所有这些合理化,除了这个。但我明白你在说什么,这是未知的。我在这里问我的问题是因为我认为有人可能知道答案。或者我对同步块如何工作的理解不正确
  • @MartinR 我的期望正确吗?执行后应该减少吗?
  • 从那个链接,bbum 还提到:“一般来说,您应该将保留计数视为增量。您的代码导致保留计数增加和减少。您不't +alloc 保留计数为 1 的对象。相反,您 +alloc 保留计数为 +1 的对象。如果您希望该对象消失,您需要做一些事情——释放,总是和最终——导致保留计数减 1。”这正是我正在做的。所以这是一个有效的用例。
  • 请注意,如果代码在发布模式下编译,即进行优化,则保留计数的行为与您预期的一样。

标签: swift multithreading memory-management grand-central-dispatch retaincount


【解决方案1】:

一些想法:

  1. 如果您对 ARC 在幕后保留和释放的确切位置有疑问,只需在“同步后内部函数”之后添加断点,运行它,当它停止时使用“调试”»“调试工作流程”»“始终Show Disassembly”,你可以看到装配,准确地看到发生了什么。我还建议在发布/优化版本中执行此操作。

    查看程序集,版本位于您的 foo 方法的末尾。

  2. 正如您所指出的,如果您将 DispatchQueue.global().sync 调用更改为 async,您会看到预期的行为。

    此外,不出所料,如果您执行函数分解,将 GCD sync 调用移动到单独的函数中,您将再次看到您所期望的行为。

  3. 你说:

    一个函数会增加它所针对的对象的保留计数

    为了澄清发生了什么,我建议您观看 WWDC 2018 What’s New in Swift,大约 12:43 进入视频,他们在视频中讨论了编译器在哪里插入 retainrelease 调用,以及如何它在 Swift 4.2 中发生了变化。

    在 Swift 4.1 中,它使用“Owned”调用约定,调用者会在调用函数之前保留对象,而被调用函数负责在返回之前执行释放。

    在 4.2 中(如下面的 WWDC 屏幕快照所示),他们实现了“保证”调用约定,消除了许多多余的保留和释放调用:

    这至少可以优化构建,生成更高效、更紧凑的代码。因此,进行发布构建并查看程序集,您会看到它的实际效果。

  4. 现在,我们来到您问题的根源,为什么 GCD sync 函数的行为与其他场景不同(例如,它的释放调用插入到与其他非转义闭包场景不同的位置)。

    这似乎与 GCD sync 独有的优化有关。具体来说,当您同步调度到某个后台队列时,而不是停止当前线程,然后在指定队列的工作线程之一上运行代码,编译器足够聪明地确定当前线程将处于空闲状态并且它将如果可以的话,只需在当前线程上运行分派的代码。我可以很容易地想象,这个 GCD sync 优化可能在编译器插入 release 调用的逻辑中引入了皱纹。

恕我直言,发布是在方法结束时完成而不是在关闭结束时完成的事实有点学术问题。我假设他们有充分的理由(或至少是实际的理由)将其推迟到函数的末尾。重要的是,当您从foo 返回时,保留计数应为该值。

【讨论】:

  • 为避免混淆,您介意在我的问题中将sync() 重命名为doSyncronously吗?实际上,我自己也对此感到困惑。我也可以编辑您的答案...请告诉我。那个截图很棒我必须看那个视频,但我完全明白你的意思,release 可以移动/推迟到函数的末尾
猜你喜欢
  • 2020-07-13
  • 2011-11-14
  • 1970-01-01
  • 2021-10-02
  • 2021-08-10
  • 2019-08-31
  • 1970-01-01
  • 1970-01-01
  • 2011-10-27
相关资源
最近更新 更多