50

I have a long running loop I want to run in the background with an NSOperation. I'd like to use a block:

NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
   while(/* not canceled*/){
      //do something...
   }
}];

The question is, how to I check to see if it's canceled. The block doesn't take any arguments, and operation is nil at the time it's captured by the block. Is there no way to cancel block operations?

jemmons
  • 18,605
  • 8
  • 55
  • 84

4 Answers4

73

Doh. Dear future googlers: of course operation is nil when copied by the block, but it doesn't have to be copied. It can be qualified with __block like so:

//THIS MIGHT LEAK! See the update below.
__block NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
   while( ! [operation isCancelled]){
      //do something...
   }
}];

UPDATE:

Upon further meditation, it occurs to me that this will create a retain cycle under ARC. In ARC, I believe __block storage is retained. If so, we're in trouble, because NSBlockOperation also keeps a strong references to the passed in block, which now has a strong reference to the operation, which has a strong reference to the passed in block, which…

It's a little less elegant, but using an explicit weak reference should break the cycle:

NSBlockOperation *operation = [[NSBlockOperation alloc] init];
__weak NSBlockOperation *weakOperation = operation;
[operation addExecutionBlock:^{
   while( ! [weakOperation isCancelled]){
      //do something...
   }
}];

Anyone that has ideas for a more elegant solution, please comment!

jemmons
  • 18,605
  • 8
  • 55
  • 84
  • 1
    Very useful! You have a typo, though: isCanceled must be isCancelled – hsdev Feb 02 '12 at 23:29
  • Fixed! Thanks. I have CodeRunner now to save me from these embarrassments in the future ;-) – jemmons Feb 03 '12 at 04:36
  • 2
    Isn't there a bug in this implementation? When weakOperation becomes nil won't it try to continue looping? i.e. !nil == true. Shouldn't the loop condition be while (weakOperation && ![weakOperation isCancelled]) ? – Marc Palmer Sep 30 '13 at 10:07
  • 2
    @MarcPalmer Given that this block is run in the the operation `weakOperation` refers to, I don't think it's possible for `weakOperation` to be nil inside of it. Certainly I haven't been able manufacture such a situation in test code. Do you have an example? – jemmons Oct 01 '13 at 02:33
  • Assume that the parent of this operation is deallocated. Therefore, operation deallocated too. Here is an example of the such situation. @MarcPalmer rights. In this situation here is a memory leak at least. – skywinder Dec 13 '13 at 19:24
  • Can you update your question to add a response? How do you, in your `// Do something section` do a dispatch_after(10 seconds) { // reference weakoperation } ? weakoperation seems to get dereferenced. How do i keep a reference in the dispatch_async block? – Just a coder Oct 03 '15 at 08:38
  • @jai `weakOperation` is just a weak (that is to say, non-retaining) reference to `operation`. 10 seconds is a lot of time. It's completely possible that `operation` could have been completed and deallocated after 10 seconds, thus `weakOperation` would be also. So I'd say your code is probably behaving as expected? If what you're trying to do is pause the operation for 10 seconds then continue, use a synchronous timeout instead of the async `dispatch_after`. Just don't do it on the main thread! ;-) – jemmons Oct 03 '15 at 14:01
  • @jemmons basically here is my question -> https://stackoverflow.com/questions/32920793/how-to-work-with-nsoperationqueue-and-nsblockoperation-with-dispatch-gcd . I used your advice here but cant solve 2 problems – Just a coder Oct 03 '15 at 23:31
  • How does the `__weak` also carry the functionality of `__block`? Is it inherent in its nature? – mfaani May 23 '16 at 17:30
  • 2
    @asma22 `__block` wouldn't be needed either way. It's used to signal that a variable imported into a block should be *mutable*. We don't need `weakOperation` to be mutable because: A) it's a pointer. We can mess with what it points to without mutating it. It'd only need to be mutable if we wanted it to point to something else. And B) even if it weren't, we're only reading from it, so it still wouldn't need to be mutable. See [Apple's docs](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Blocks/Articles/bxVariables.html#//apple_ref/doc/uid/TP40007502-CH6-SW6) – jemmons May 24 '16 at 14:50
  • Why not set a property to the NSBlockOperation? Then, you can do something like: while( ! [self.op isCancelled]) { //do something } – Victor Engel Nov 07 '16 at 17:30
  • Anyone knows if the execution block can 'cancel itself' (say if it encounters an error) setting the weak operation.isCancelled to YES then exit ??? Will dependent operations be cancelled too as expected? – Motti Shneor Nov 05 '20 at 12:45
47

To reinforce jemmons answer. WWDC 2012 session 211 - Building Concurent User Interfaces (33 mins in)

NSOperationQueue* myQueue = [[NSOperationQueue alloc] init];
NSBlockOperation* myOp = [[NSBlockOperation alloc] init];

// Make a weak reference to avoid a retain cycle
__weak NSBlockOperation* myWeakOp = myOp;

[myOp addExecutionBlock:^{
    for (int i = 0; i < 10000; i++) {
        if ([myWeakOp isCancelled]) break;
        precessData(i);
    }
}];
[myQueue addOperation:myOp];
Robert
  • 37,670
  • 37
  • 171
  • 213
  • Just want to make sure you can't do blockOperationWithBlock on this one do you? – user4951 May 07 '13 at 01:56
  • 1
    `blockOperationWithBlock` is normally very convenient but unfortunately you can not get a reference to the operation when you use this method (well actually you can get one after you declare it, but you can't use this reference in the actual block). You need a reference to check if the operation is canceled. – Robert May 07 '13 at 09:51
  • I managed to pull that out but then the block operation need to be declared as __weak __block so the block store a reference to it rather than copy the actual pointer. – user4951 May 07 '13 at 10:31
  • and the result is just as complicated. – user4951 Sep 10 '13 at 04:24
  • Its probably safer to write `if (!myWeakOp || [myWeakOp isCancelled])` to prevent executing the body if the block is actually nil :) – Daniel Galasko Jul 16 '15 at 11:17
13

With Swift 5, you can create a cancellable BlockOperation with addExecutionBlock(_:). addExecutionBlock(_:) has the following declaration:

func addExecutionBlock(_ block: @escaping () -> Void)

Adds the specified block to the receiver’s list of blocks to perform.


The example below shows how to implement addExecutionBlock(_:):

let blockOperation = BlockOperation()

blockOperation.addExecutionBlock({ [unowned blockOperation] in
    for i in 0 ..< 10000 {
        if blockOperation.isCancelled {
            print("Cancelled")
            return // or break
        }
        print(i)
    }
})

Note that, in order to prevent a retain cycle between the BlockOperation instance and its execution block, you have to use a capture list with a weak or unowned reference to blockOperation inside the execution block.


The following Playground code shows how to cancel a BlockOperation subclass instance and check that there is no retain cycle between it and its execution block:

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

class TestBlockOperation: BlockOperation {
    deinit {
        print("No retain cycle")
    }
}

do {
    let queue = OperationQueue()

    let blockOperation = TestBlockOperation()
    blockOperation.addExecutionBlock({ [unowned blockOperation] in
        for i in 0 ..< 10000 {
            if blockOperation.isCancelled {
                print("Cancelled")
                return // or break
            }
            print(i)
        }
    })

    queue.addOperation(blockOperation)

    Thread.sleep(forTimeInterval: 0.5)
    blockOperation.cancel()
}

This prints:

0
1
2
3
...
Cancelled
No retain cycle
Imanou Petit
  • 89,880
  • 29
  • 256
  • 218
0

I wanted to have cancellable blocks that my UICollectionViewController could easily cancel once cells were scrolled off the screen. The blocks are not doing network ops, they are doing image operations (resizing, cropping etc). The blocks themselves need to have a reference to check if their op has been cancelled, and none of the other answers (at the time I wrote this) provided that.

Here's what worked for me (Swift 3) - making blocks that take a weak ref to the BlockOperation, then wrapping them in the BlockOperation block itself:

    public extension OperationQueue {
        func addCancellableBlock(_ block: @escaping (BlockOperation?)->Void) -> BlockOperation {
            let op = BlockOperation.init()
            weak var opWeak = op
            op.addExecutionBlock {
                block(opWeak)
            }
            self.addOperation(op)
            return op
        }
    }

Using it in my UICollectionViewController:

var ops = [IndexPath:Weak<BlockOperation>]()

    func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
        ...
        ops[indexPath] = Weak(value: DispatchQueues.concurrentQueue.addCancellableBlock({ (op) in
            cell.setup(obj: photoObj, cellsize: cellsize)
        }))
    }

    func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
        if let weakOp = ops[indexPath], let op: BlockOperation = weakOp.value {
            NSLog("GCV: CANCELLING OP FOR INDEXPATH \(indexPath)")
            op.cancel()
        }
    }

Completing the picture:

    class Weak<T: AnyObject> {
        weak var value : T?
        init (value: T) {
            self.value = value
        }
    }
xaphod
  • 6,392
  • 2
  • 37
  • 45