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?


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