2

I'm trying to explain ownership of objects and how GCD does its work. These are the things I've learned:

  • a function will increase the retain count of the object its calling against
  • a dispatch block, unless it captures self weakly will increase the count.
  • after a dispatched block is executed it release the captured object, hence the retain count of self should decrease. But that's not what I'm seeing here. Why is that?
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
    }
}

Usage:

var c: C? = C()
print("before func call", CFGetRetainCount(c)) // 2
c?.foo()
print("after func call", CFGetRetainCount(c)) // 2
mfaani
  • 33,269
  • 19
  • 164
  • 293
  • There are many reasons why the retain count can be different from what you expect. Compare https://stackoverflow.com/questions/4636146/when-to-use-retaincount, http://www.friday.com/bbum/2011/12/18/retaincount-is-useless/, or http://sdarlington.github.io. – In your case, the behavior is different between Debug and Release mode. – Martin R Oct 14 '19 at 14:22
  • @MartinR FWIW I tested this with async blocks as well that captured `self` in a non-`weak` way, with async blocks that captured self `weak`ly, or calling multiple async blocks. The increase/decrease in retain count and I was able to rationalize for all of them except for this one. But I get what you're saying, it's unknown. I was asking my question here because I thought someone may know the answer. Or maybe my understanding of how sync blocks work is incorrect – mfaani Oct 14 '19 at 14:26
  • @MartinR is my expectation correct? That it should have decreased after it’s executed? – mfaani Oct 14 '19 at 16:08
  • From that link, bbum also mentioned: "In general, **you should consider the retain count as a delta**. Your code causes the retain count to increase and decrease. You don’t +alloc an object with a retain count of 1. Instead, you +alloc an object with a retain count of +1. If you want that object to go away, you need to do something — release, always and eventually — that causes the retain count to be decremented by 1." That's exactly what I'm doing. So it's a valid use case here. – mfaani Oct 14 '19 at 20:11
  • Note that the retain count behaves as you expect if the code is compiled in Release mode, i.e. with optimizations. – Martin R Oct 14 '19 at 20:17
  • I see. I'll validate that in a big...Thanks – mfaani Oct 14 '19 at 21:03
  • @MartinR I dumped the code into a project. Tried with a debug scheme. The results same as playground. Then I tried with a Release scheme. Every retain count was reduced by 1. So still it doesn't make sense. Did you try it yourself? – mfaani Oct 16 '19 at 11:20
  • Yes I did. But I can double-check it later. – Martin R Oct 16 '19 at 11:33
  • few years old discussion could answer your question https://stackoverflow.com/q/4636146/3441734 – user3441734 Oct 27 '19 at 07:09
  • or check this answer https://stackoverflow.com/a/38367647/3441734 – user3441734 Oct 27 '19 at 07:13

1 Answers1

1

A couple of thoughts:

  1. If you ever have questions about precisely where ARC is retaining and releasing behind the scenes, just add breakpoint after “inside func after sync”, run it, and when it stops use “Debug” » “Debug Workflow” » “Always Show Disassembly” and you can see the assembly, to precisely see what’s going on. I’d also suggest doing this with release/optimized builds.

    Looking at the assembly, the releases are at the end of your foo method.

  2. As you pointed out, if you change your DispatchQueue.global().sync call to be async, you see the behavior you’d expect.

    Also, unsurprisingly, if you perform functional decomposition, moving the GCD sync call into a separate function, you’ll again see the behavior you were expecting.

  3. You said:

    a function will increase the retain count of the object its calling against

    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.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • To avoid confusion, do you mind if in my question I rename the `sync()` to `doSyncronously`? I was actually confused about that myself too. I can edit your answer too...just let me know. That screenshot is great. I'll have to see that video, but I totally get what you're saying that the `release` could be moved/deferred to then end of the function – mfaani Mar 29 '20 at 16:46