Skip to content

Instantly share code, notes, and snippets.

@mfaani
Created April 1, 2020 14:42
Show Gist options
  • Save mfaani/639c8ebd8b613c45cb07070f6f7c8b29 to your computer and use it in GitHub Desktop.
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

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
Copy link

winkelsdorf commented Dec 10, 2020

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