How to cancel NSBlockOperation

20,218

Solution 1

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!

Solution 2

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];

Solution 3

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
Share:
20,218
jemmons
Author by

jemmons

I write comic books. And iOS apps.

Updated on July 14, 2022

Comments

  • jemmons
    jemmons almost 2 years

    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?

  • hsdev
    hsdev about 12 years
    Very useful! You have a typo, though: isCanceled must be isCancelled
  • jemmons
    jemmons about 12 years
    Fixed! Thanks. I have CodeRunner now to save me from these embarrassments in the future ;-)
  • user4951
    user4951 almost 11 years
    Just want to make sure you can't do blockOperationWithBlock on this one do you?
  • Robert
    Robert almost 11 years
    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.
  • user4951
    user4951 almost 11 years
    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
    user4951 over 10 years
    and the result is just as complicated.
  • Anton Tb
    Anton Tb over 10 years
    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]) ?
  • jemmons
    jemmons over 10 years
    @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?
  • skywinder
    skywinder over 10 years
    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.
  • Daniel Galasko
    Daniel Galasko almost 9 years
    Its probably safer to write if (!myWeakOp || [myWeakOp isCancelled]) to prevent executing the body if the block is actually nil :)
  • Just a coder
    Just a coder over 8 years
    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?
  • jemmons
    jemmons over 8 years
    @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! ;-)
  • Just a coder
    Just a coder over 8 years
    @jemmons basically here is my question -> stackoverflow.com/questions/32920793/… . I used your advice here but cant solve 2 problems
  • mfaani
    mfaani almost 8 years
    How does the __weak also carry the functionality of __block? Is it inherent in its nature?
  • jemmons
    jemmons almost 8 years
    @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
  • Victor Engel
    Victor Engel over 7 years
    Why not set a property to the NSBlockOperation? Then, you can do something like: while( ! [self.op isCancelled]) { //do something }
  • Motti Shneor
    Motti Shneor over 3 years
    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?