3

After learning about Swift's capture list and how it can be used to avoid retain cycle, I can't help noticing something puzzling about OperationQueue: it doesn't need either [weak self] or [unowned self] to prevent memory leak.

class SomeManager {
    let queue = OperationQueue()
    let cache: NSCache = { () -> NSCache<AnyObject, AnyObject> in
        let cache = NSCache<AnyObject, AnyObject>()
        cache.name = "huaTham.TestOperationQueueRetainCycle.someManager.cache"
        cache.countLimit = 16
        return cache
    }()

    func addTask(a: Int) {
        queue.addOperation { // "[unowned self] in" not needed?
            self.cache.setObject(a as AnyObject, forKey: a as AnyObject)
            print("hello \(a)")
        }
    }
}

class ViewController: UIViewController {

    var someM: SomeManager? = SomeManager()

    override func viewDidLoad() {
        super.viewDidLoad()
        someM?.addTask(a: 1)
        someM?.addTask(a: 2)
    }

    // This connects to a button.
    @IBAction func invalidate() {
        someM = nil  // Perfectly fine here. No leak.
    }
}

I don't see why adding an operation would not cause a retain cycle: SomeManager strongly owns the queue, which in turns strongly owns the added closures. Each added closure strongly refers back to SomeManager. This should theoretically create a retain cycle leading to memory leak. Yet Instruments shows that everything is perfectly fine.

No Leak

Why is this the case? In some other multi-threaded, block-based APIs, like DispatchSource, you seem to need the capture list. See Apple's sample code ShapeEdit for example, in ThumbnailCache.swift:

fileprivate var flushSource: DispatchSource
...
flushSource.setEventHandler { [weak self] in   // Here
    guard let strongSelf = self else { return }

    strongSelf.delegate?.thumbnailCache(strongSelf, didLoadThumbnailsForURLs: strongSelf.URLsNeedingReload)
    strongSelf.URLsNeedingReload.removeAll()
}

But in the same code file, OperationQueue doesn't need the capture list, despite having the same semantics: you hand over a closure with reference to self to be executed asynchronously:

fileprivate let workerQueue: OperationQueue { ... }
...
self.workerQueue.addOperation {
    if let thumbnail = self.loadThumbnailFromDiskForURL(URL) {
        ...
        self.cache.setObject(scaledThumbnail!, forKey: documentIdentifier as AnyObject)
    }
}

I have read about Swift's capture list above, as well as related SO answers like this and this and this, but I still don't know why [weak self] or [unowned self] are not needed in OperationQueue API while they are in Dispatch API. I'm also not sure how no leaks are found in OperationQueue case.

Any clarifications would be much appreciated.

Edit

In addition to the accepted answer below, I also find the comment by QuinceyMorris in Apple forums quite helpful.

HuaTham
  • 7,486
  • 5
  • 31
  • 50

1 Answers1

5

You do have a retain cycle there, but that doesn’t automatically lead to a memory leak. After the queue finished the operation it releases it thus breaking the cycle.

Such an temporary retain cycle can be very useful in some situations as you won’t have to hang on to an object and still have it finish its work.

As an experiment you can suspend the queue. Then you will see the memory leak.

Sven
  • 22,475
  • 4
  • 52
  • 71
  • But how do you release the operation? From [Apple's ARC guide](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html#//apple_ref/doc/uid/TP40014097-CH20-ID52), in the `Person` and `Apartment` example, once you have a strong reference cycle, it seems you won't ever be able to release any memory even when you set both references to `nil`. – HuaTham Jan 08 '18 at 03:45
  • You must have misunderstood that. As soon as one of the strong references in a cycle is set to nil, the rest will be cleaned up automatically. You just can’t use `deinit` to do that because that won’t run before the cycle is broken. – Sven Jan 08 '18 at 13:41
  • With all due respect, I don't think I misunderstood this. It's clearly stated that "neither deinitializer was called when you set these two variables to nil. The strong reference cycle prevents the `Person` and `Apartment` instances from ever being deallocated, causing a memory leak in your app." So that means as soon as you have a strong reference cycle, you will have a memory leak. That's why I'm wondering why `weak` / `unowned` aren't being used for `OperationQueue`. Same thing for `HTMLElement` example in the same guide. – HuaTham Jan 08 '18 at 18:42
  • 1
    If you don’t believe me try it yourself. Here is an example program that first forms a cycle and then breaks it: https://gist.github.com/5sw/819bdae89fb6e5ef21355adf982554f8 The example from the ARC guide is also correct though. If you don’t have access to any object in the cycle any more you can’t break it. So if you take the example from my gist and also set the weak reference `a` to nil you can’t break the cycle any more and you do leak memory. But this doesn’t happen with your operation queue. – Sven Jan 08 '18 at 19:05
  • Thank you for the comment and the answer. I will have a look at this some time and let you know. – HuaTham Jan 12 '18 at 06:20
  • Your example does indeed demonstrate that the retain cycle can be manually broken. The analogous line in Apple's example would be `john?.apartment = nil` and the retain cycle would break. Thanks again! – HuaTham Jan 14 '18 at 09:54