Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I create a reference cycle using dispatchQueues?

I feel that I've always misunderstood that when reference cycles are created. Before I use to think that almost any where that you have a block and the compiler is forcing you to write .self then it's a sign that I'm creating a reference cycle and I need to use [weak self] in.

But the following setup doesn't create a reference cycle.

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution


class UsingQueue {
    var property : Int  = 5
    var queue : DispatchQueue? = DispatchQueue(label: "myQueue")

    func enqueue3() {
        print("enqueued")
        queue?.asyncAfter(deadline: .now() + 3) {
            print(self.property)
        }
    }

    deinit {
        print("UsingQueue deinited")
    }
}

var u : UsingQueue? = UsingQueue()
u?.enqueue3()
u = nil

The block only retains self for 3 seconds. Then releases it. If I use async instead of asyncAfter then it's almost immediate.

From what I understand the setup here is:

self ---> queue
self <--- block

The queue is merely a shell/wrapper for the block. Which is why even if I nil the queue, the block will continue its execution. They’re independent.

So is there any setup that only uses queues and creates reference cycles?

From what I understand [weak self] is only to be used for reasons other than reference cycles ie to control the flow of the block. e.g.

Do you want to retain the object and run your block and then release it? A real scenario would be to finish this transaction even though the view has been removed from the screen...

Or you want to use [weak self] in so that you can exit early if your object has been deallocated. e.g. some purely UI like stopping a loading spinner is no longer needed


FWIW I understand that if I use a closure then things are different ie if I do:

import PlaygroundSupport
import Foundation

PlaygroundPage.current.needsIndefiniteExecution
class UsingClosure {
    var property : Int  = 5

    var closure : (() -> Void)?

    func closing() {
        closure = {
            print(self.property)
        }
    }

    func execute() {
        closure!()
    }
    func release() {
        closure = nil
    }


    deinit {
        print("UsingClosure deinited")
    }
}


var cc : UsingClosure? = UsingClosure()
cc?.closing()
cc?.execute()
cc?.release() // Either this needs to be called or I need to use [weak self] for the closure otherwise there is a reference cycle
cc = nil

In the closure example the setup is more like:

self ----> block
self <--- block

Hence it's a reference cycle and doesn't deallocate unless I set block to capturing to nil.

EDIT:

class C {
    var item: DispatchWorkItem!
    var name: String = "Alpha"

    func assignItem() {
        item = DispatchWorkItem { // Oops!
            print(self.name)
        }
    }

    func execute() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: item)
    }

    deinit {
        print("deinit hit!")
    }
}

With the following code, I was able to create a leak ie in Xcode's memory graph I see a cycle, not a straight line. I get the purple indicators. I think this setup is very much like how a stored closure creates leaks. And this is different from your two examples, where execution is never finished. In this example execution is finished, but because of the references it remains in memory.

I think the reference is something like this:

┌─────────┐─────────────self.item──────────────▶┌────────┐
│   self  │                                     │workItem│
└─────────┘◀︎────item = DispatchWorkItem {...}───└────────┘

enter image description here

like image 217
mfaani Avatar asked May 09 '19 13:05

mfaani


Video Answer


1 Answers

You say:

From what I understand the setup here is:

self ---> queue
self <--- block

The queue is merely a shell/wrapper for the block. Which is why even if I nil the queue, the block will continue its execution. They’re independent.

The fact that self happens to have a strong reference to the queue is inconsequential. A better way of thinking about it is that a GCD, itself, keeps a reference to all dispatch queues on which there is anything queued. (It’s analogous to a custom URLSession instance that won’t be deallocated until all tasks on that session are done.)

So, GCD keeps reference to the queue with dispatched tasks. The queue keeps a strong reference to the dispatched blocks/items. The queued block keeps a strong reference to any reference types they capture. When the dispatched task finishes, it resolves any strong references to any captured reference types and is removed from the queue (unless you keep your own reference to it elsewhere.), generally thereby resolving any strong reference cycles.


Setting that aside, where the absence of [weak self] can get you into trouble is where GCD keeps a reference to the block for some reason, such as dispatch sources. The classic example is the repeating timer:

class Ticker {
    private var timer: DispatchSourceTimer?

    func startTicker() {    
        let queue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".ticker")
        timer = DispatchSource.makeTimerSource(queue: queue)
        timer!.schedule(deadline: .now(), repeating: 1)
        timer!.setEventHandler {                         // whoops; missing `[weak self]`
            self.tick()
        }
        timer!.resume()
    }

    func tick() { ... }
}

Even if the view controller in which I started the above timer is dismissed, GCD keeps firing this timer and Ticker won’t be released. As the “Debug Memory Graph” feature shows, the block, created in the startTicker routine, is keeping a persistent strong reference to the Ticker object:

repeating timer memory graph

This is obviously resolved if I use [weak self] in that block used as the event handler for the timer scheduled on that dispatch queue.

Other scenarios include a slow (or indefinite length) dispatched task, where you want to cancel it (e.g., in the deinit):

class Calculator {
    private var item: DispatchWorkItem!

    deinit {
        item?.cancel()
        item = nil
    }

    func startCalculation() {
        let queue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".calcs")
        item = DispatchWorkItem {                         // whoops; missing `[weak self]`
            while true {
                if self.item?.isCancelled ?? true { break }
                self.calculateNextDataPoint()
            }
            self.item = nil
        }
        queue.async(execute: item)
    }

    func calculateNextDataPoint() {
        // some intense calculation here
    }
}

dispatch work item memory graph

All of that having been said, in the vast majority of GCD use-cases, the choice of [weak self] is not one of strong reference cycles, but rather merely whether we mind if strong reference to self persists until the task is done or not.

  • If we’re just going to update the the UI when the task is done, there’s no need to keep the view controller and its views in the hierarchy waiting some UI update if the view controller has been dismissed.

  • If we need to update the data store when the task is done, then we definitely don’t want to use [weak self] if we want to make sure that update happens.

  • Frequently, the dispatched tasks aren’t consequential enough to worry about the lifespan of self. For example, you might have a URLSession completion handler dispatch UI update back to the main queue when the request is done. Sure, we theoretically would want [weak self] (as there’s no reason to keep the view hierarchy around for a view controller that’s been dismissed), but then again that adds noise to our code, often with little material benefit.


Unrelated, but playgrounds are a horrible place to test memory behavior because they have their own idiosyncrasies. It’s much better to do it in an actual app. Plus, in an actual app, you then have the “Debug Memory Graph” feature where you can see the actual strong references. See https://stackoverflow.com/a/30993476/1271826.

like image 136
Rob Avatar answered Sep 20 '22 17:09

Rob