How to stop a DispatchWorkItem in GCD?
Asked Answered
I

3

40

I am currently playing around with Grand Central Dispatch and discovered a class called DispatchWorkItem. The documentation seems a little incomplete so I am not sure about using it the right way. I created the following snippet and expected something different. I expected that the item will be cancelled after calling cancel on it. But the iteration continues for some reason. Any ideas what I am doing wrong? The code seems fine for me.

@IBAction func testDispatchItems() {
    let queue = DispatchQueue.global(attributes:.qosUserInitiated)
    let item = DispatchWorkItem { [weak self] in
        for i in 0...10000000 {
            print(i)
            self?.heavyWork()
        }
    }

    queue.async(execute: item)
    queue.after(walltime: .now() + 2) {
        item.cancel()
    }
}
Ischium answered 14/7, 2016 at 9:8 Comment(0)
R
75

GCD does not perform preemptive cancelations. So, to stop a work item that has already started, you have to test for cancelations yourself. In Swift, cancel the DispatchWorkItem. In Objective-C, call dispatch_block_cancel on the block you created with dispatch_block_create. You can then test to see if was canceled or not with isCancelled in Swift (known as dispatch_block_testcancel in Objective-C).

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

    var item: DispatchWorkItem?

    // create work item

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

    // start it

    queue.async(execute: item!)

    // after five seconds, stop it if it hasn't already

    queue.asyncAfter(deadline: .now() + 5) {
        item?.cancel()
        item = nil
    }
}

Or, in Objective-C:

- (void)testDispatchItem {
    dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);

    static dispatch_block_t block = nil;  // either static or property

    __weak typeof(self) weakSelf = self;

    block = dispatch_block_create(0, ^{
        for (long i = 0; i < 10000000; i++) {
            if (dispatch_block_testcancel(block)) { break; }
            NSLog(@"%ld", i);
            [weakSelf heavyWork];
        }

        block = nil;
    });

    // start it

    dispatch_async(queue, block);

    // after five seconds, stop it if it hasn't already

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        if (block) { dispatch_block_cancel(block); }
    });
}
Repletion answered 14/7, 2016 at 10:57 Comment(17)
Can DispatchWorkItem be reused after cancelled?Seduce
@Seduce - No. Create a new one if you want to start it again.Repletion
@Repletion - doesn't this create a retain cycle?Uppity
@Uppity - Good catch. You're absolutely right. Unfortunately, you can't use [weak item] or [unowned item] patterns when instantiating the DispatchWorkItem (the typical method for resolving strong reference cycles), so you have to manually nil the item at the end of the closure like I have in the revised answer. (But we can use the typical [weak item] pattern in the block where we asyncAfter the cancel.)Repletion
@Repletion That was my first thought too. Unfortunately if the DispatchItem is cancelled before it is executed, the memory is leaked.Uppity
Why do you have do item = nil at the end? The docs say: Take care to ensure that a work item does not capture any resources that require execution of the block body in order to be released Understandably the docs continue and say Such resources are leaked if the block body is never executed due to cancellation. Doesn't that imply that execution would release the instance? Just like how Dispatch.main.async releases upon execution? Or this is somehow different?!Heather
@Honey Why set it to nil? Because if you don’t do that, you’ll leak (see it under libswiftDispatch.dylib when you debug your memory graph). If you don’t want to leak if canceled before it starts, set item to nil when you cancel, too. FWIW, you could save this DispatchWorkItem as a property, rather than local var, and use a weak reference to self. But you’ll still need to nil that property when it’s done or is canceled, or else you’ll leak again. Operation is far more graceful and avoids these memory management nuisances.Repletion
@Uppity - If you don’t want it to leak if you cancel before it starts, set the reference to nil when you cancel.Repletion
@Repletion I wrapped your function into a class, then I removed the item = nil // resolve ... + queue.asyncAfter(deadline: .now() + 5) { ...and it deallocated for both when I let it execute or when I canceled. When using [weak self] it all just works for me. Can you make sure if you actually cause a leak?Heather
Great idea. Please see here. For future can you mention my name so I'd be notified?Heather
@Honey - That example leaks the DispatchWorkItem: That’s a great example of why we need to set item to nil.Repletion
Ahhh I see. Thanks a lot. I suppose if I remove the item?.isCancelled from within the block then the DispatchWorkItem's_leaking_ problem is resolved, but then you have other problems. When I read // resolve strong reference cycle this whole time I was thinking that I was leaking the Foo class. But you were talking about the DispatchWorkItem itself which can leak. Do you mind editing it and making it more clear?Heather
I’ve added qualifier “of the DispatchWorkItem” to that code comment...Repletion
@Repletion How could the leak be prevented, if we are storing the DispatchWorkItem as a property in an outer class (in the gist Foo). I am cancelling previously running work items in the method first, but I'm unable to avoid the leak when cancelling the current operation which had not been running at all. Added a comment to @Honey's gist.Abrego
Personally, if I had a bunch of tasks that were being queued up like this, I would first stop and ask whether this is even the right pattern. For example, I might consider operation queue instead, which has a cancellAllOperations. Or if using Combine, if you have Set of AnyCancellable objects, you can just empty the set and they'll be canceled. DispatchWorkItem is not the first tool that I would reach for...Repletion
@Repletion have you thought about using: guard let strongSelf = self else { return } when: [weak self] ?Iodate
I would use self rather than strongSelf (e.g., guard let self = self else { return }) per SE-0079, but, yes, I frequently use that pattern if I have multiple self references or if it makes it more clear. But I often use the nil-coalescing operator in simple situations. Use whichever you prefer.Repletion
P
1

DispatchWorkItem without DispatchQueue

 let workItem = DispatchWorkItem{
    //write youre code here
 }
 workItem.cancel()// For Stop

DispatchWorkItem with DispatchQueue

let workItem = DispatchWorkItem{
   //write youre code here
}
DispatchQueue.main.async(execute: workItem)
workItem.cancel()// For Stop

Execute

workItem.perform()// For Execute
workItem.wait()// For Delay Execute
Perdita answered 14/11, 2022 at 12:40 Comment(0)
U
0

There is no asynchronous API where calling a "Cancel" method will cancel a running operation. In every single case, a "Cancel" method will do something so the operation can find out whether it is cancelled, and the operation must check this from time to time and then stop doing more work by itself.

I don't know the API in question, but typically it would be something like

        for i in 0...10000000 {
            if (self?.cancelled)
                break;

            print(i)
            self?.heavyWork()
        }
Uptotheminute answered 14/7, 2016 at 9:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.