Skip to content

Instantly share code, notes, and snippets.

@mfaani
Created April 1, 2020 14:42
Show Gist options
  • Select an option

  • Save mfaani/639c8ebd8b613c45cb07070f6f7c8b29 to your computer and use it in GitHub Desktop.

Select an option

Save mfaani/639c8ebd8b613c45cb07070f6f7c8b29 to your computer and use it in GitHub Desktop.
Related to https://stackoverflow.com/a/38372384/5175709 Trying to show setting `item` to `nil` doesn't seem to be necessary
import Foundation
class Foo {
func testDispatchItems() {
let queue = DispatchQueue.global()
var item: DispatchWorkItem?
item = DispatchWorkItem { [weak self] in
for i in 0 ... 10 {
if item?.isCancelled ?? true { break }
print(i)
self?.heavyWork(i)
}
// item = nil // resolve strong reference cycle
}
item?.cancel()
}
func heavyWork(_ num: Int) {
print(num)
}
deinit {
print("deallocated")
}
}
var c: Foo? = Foo()
c?.testDispatchItems()
c = nil
// no need to set item to `nil` when execution finishes...
/*
Output:
deallocated
*/
import Foundation
class Foo {
func testDispatchItems() {
let queue = DispatchQueue.global()
var item: DispatchWorkItem?
item = DispatchWorkItem { [weak self] in
for i in 0 ... 10 {
if item?.isCancelled ?? true { break }
print(i)
self?.heavyWork(i)
}
// item = nil // resolve strong reference cycle
}
queue.async(execute: item!)
queue.asyncAfter(deadline: .now() + 5) {
item?.cancel()
item = nil
}
}
func heavyWork(_ num: Int) {
print(num)
}
deinit {
print("deallocated")
}
}
var c: Foo? = Foo()
c?.testDispatchItems()
c = nil
// no need to set item to `nil` when execution finishes...
/*
Output:
0
0
1
1
deallocated
2
3
4
5
6
7
8
9
10
*/
@robertmryan

Copy link
Copy Markdown

The problem isn’t the container class, Foo, but rather the DispatchWorkItem.

I ran the DispatchWorkItemCanceled and when it was done and I received the deallocated message, I tapped the “debug memory graph” button, debugmemorygraph, and I can see that the DispatchWorkItem leaked:

Screen Shot 2020-04-01 at 8 14 48 AM (2)

To fix this “cancel” scenario, I had to nil the item after I cancel it:

class Foo {
    func testDispatchItems() {
        let queue = DispatchQueue.global()

        var item: DispatchWorkItem?

        item = DispatchWorkItem { [weak self] in
            for i in 0 ... 10 {
                if item?.isCancelled ?? true { break }
                print(i)
                self?.heavyWork(i)
            }
            item = nil    // resolve strong reference cycle
        }

        item?.cancel()
        item = nil
    }

    func heavyWork(_ num: Int) {
        print(num)
    }

    deinit {
        print("deallocated")
    }
}

In the DispatchWorkItemExecuted example, while you commented out the item = nil inside the block, you are setting item to nil where you cancel the block, which is resolves the issue. But if you commented out that second item = nil, too, you again would see a leak:

Screen Shot 2020-04-01 at 8 41 46 AM (2)

This is again resolved by setting item to nil.

@winkelsdorf

winkelsdorf commented Dec 10, 2020

Copy link
Copy Markdown

What about the following example, I'm trying to cancel previously running work items (if there had been any). I'm unable to avoid a leak when not running the operation at all (i.e. Cancel immediately). Likely I'm too focussed on this now.

Edit: Looks like it's the Work Item inside testDispatchItems which leaks. That one must be nilled. Still leaking at least once.

class ViewController: UIViewController {

    lazy var foo = Foo()

    override func viewDidLoad() {
        super.viewDidLoad()

        foo.testDispatchItems()

        // fire a 2nd run
        DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) { [weak self] in
            self?.foo.testDispatchItems()
        }
    }

}

class Foo {
    
    var item: DispatchWorkItem?
    
    func testDispatchItems() {
        // cancel previously running work items, if any
        self.item?.cancel()
        self.item = nil
        
        var workItem: DispatchWorkItem?

        workItem = DispatchWorkItem { [weak self] in
            defer {
                debugPrint("defer")
                // workItem = nil // does not help
            }
            
            for i in 0 ... 50 {
                if workItem?.isCancelled ?? false {
                    debugPrint("Cancelled")
                    break
                }
                debugPrint("for", i)
                self?.heavyWork(i)
            }
            
            self?.item = nil // resolve strong reference cycle
        }
        
        guard let assignedWorkItem = workItem else {
            assertionFailure()
            self.item = nil
            return
        }
        
        self.item = assignedWorkItem
          
        self.item?.cancel()
        self.item = nil

//        DispatchQueue.global()
//            .async(execute: assignedWorkItem)
    }

    func heavyWork(_ num: Int) {
        debugPrint("heavyWork", num)
        Thread.sleep(forTimeInterval: 0.1)
    }

    deinit {
        print("deallocated")
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment