首页 > 解决方案 > 同步调度的保留计数如何工作?

问题描述

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

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

标签: swiftmultithreadingmemory-managementgrand-central-dispatchretaincount

解决方案


几个想法:

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

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

  2. 正如您所指出的,如果您将DispatchQueue.global().sync呼叫更改为 be async,您会看到您期望的行为。

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

  3. 你说:

    函数将增加其调用对象的保留计数

    Just to clarify what’s going on, I’d refer you to WWDC 2018 What’s New in Swift, about 12:43 into the video, in which they discuss where the compiler inserts the retain and release calls, and how it changed in Swift 4.2.

    In Swift 4.1, it used the “Owned” calling convention where the caller would retain the object before calling the function, and the called function was responsible for performing the release before returning.

    In 4.2 (shown in the WWDC screen snapshot below), they implemented a “Guaranteed” calling convention, eliminating a lot of redundant retain and release calls:

    enter image description here

    This results, in optimized builds at least, in more efficient and more compact code. So, do a release build and look at the assembly, and you’ll see that in action.

  4. Now, we come to the root of your question, as to why the GCD sync function behaves differently than other scenarios (e.g. where its release call is inserted in a different place than other scenarios with non-escaping closures).

    It seems that this is potentially related to optimizations unique to GCD sync. Specifically, when you dispatch synchronously to some background queue, rather than stopping the current thread and then running the code on one of the worker threads of the designated queue, the compiler is smart enough to determine that the current thread would be idle and it will just run the dispatched code on the current thread if it can. I can easily imagine that this GCD sync optimization, might have introduced wrinkles in the logic about where the compiler inserted the release call.

IMHO, the fact that the release is done at the end of the method as opposed to at the end of the closure is a somewhat academic matter. I’m assuming they had good reasons (or practical reasons at least), to defer this to the end of the function. What’s important is that when you return from foo, the retain count is what it should be.


推荐阅读