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()
    }
}
mfaani
  • 33,269
  • 19
  • 164
  • 293
Sebastian Boldt
  • 5,283
  • 9
  • 52
  • 64

3 Answers3

71

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); }
    });
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Can DispatchWorkItem be reused after cancelled? – pixelfreak Oct 12 '16 at 13:36
  • @pixelfreak - No. Create a new one if you want to start it again. – Rob Oct 12 '16 at 15:25
  • 1
    @Rob - doesn't this create a retain cycle? – mattsven Mar 07 '17 at 12:55
  • 2
    @mattsven - 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`.) – Rob Mar 07 '17 at 18:19
  • 1
    @Rob That was my first thought too. Unfortunately if the `DispatchItem` is cancelled before it is executed, the memory is leaked. – mattsven Mar 07 '17 at 21:00
  • Why do you have do `item = nil` at the end? The [docs](https://developer.apple.com/documentation/dispatch/dispatchworkitem/1780910-cancel) 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?! – mfaani Feb 27 '20 at 00:21
  • @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. – Rob Mar 09 '20 at 20:24
  • @mattsven - If you don’t want it to leak if you cancel before it starts, set the reference to `nil` when you cancel. – Rob Mar 09 '20 at 20:31
  • @Rob 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? – mfaani Mar 30 '20 at 19:41
  • Great idea. Please see [here](https://gist.github.com/prohoney/639c8ebd8b613c45cb07070f6f7c8b29). For future can you mention my name so I'd be notified? – mfaani Apr 01 '20 at 14:43
  • @Honey - That example leaks the `DispatchWorkItem`: That’s a great example of why we need to set `item` to `nil`. – Rob Apr 01 '20 at 16:06
  • 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? – mfaani Apr 01 '20 at 16:31
  • 1
    I’ve added qualifier “of the DispatchWorkItem” to that code comment... – Rob Apr 01 '20 at 17:34
  • @Rob 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. – Frederik Winkelsdorf Dec 10 '20 at 19:26
  • 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`](https://developer.apple.com/documentation/foundation/operationqueue/1417849-cancelalloperations). 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... – Rob Dec 11 '20 at 16:26
  • @Rob have you thought about using: `guard let strongSelf = self else { return } ` when: `[weak self]` ? – Laur Stefan May 24 '21 at 00:21
  • I would use `self` rather than `strongSelf` (e.g., `guard let self = self else { return }`) per [SE-0079](https://github.com/apple/swift-evolution/blob/main/proposals/0079-upgrade-self-from-weak-to-strong.md), 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. – Rob May 24 '21 at 00:59
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()
        }
gnasher729
  • 51,477
  • 5
  • 75
  • 98
0

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