11

I have some NSOperations in a dependency graph:

NSOperation *op1 = ...;
NSOperation *op2 = ...;

[op2 addDependency:op1];

Here's how I'm running them:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:op1];
[queue addOperation:op2];

Now I need to cancel them. How do I ensure that all the NSOperations in a dependency graph are cancelled, and that no other NSOperations are cancelled?


what I've tried:

Calling cancel on either NSOperation doesn't cancel the other (as far as I can tell):

[op1 cancel]; // doesn't cancel op2
// -- or --
[op2 cancel]; // doesn't cancel op1

Cancelling the queue would also cancel operations that aren't part of the dependency graph of op1 and op2 (if there are any such operations in the queue):

[queue cancelAllOperations];

So I solved this using a custom method that recursively looks through an NSOperation's dependencies and cancels them. However, I'm not happy with this solution because I feel like I'm fighting the framework:

- (void)recursiveCancel:(NSOperation *)op
{
    [op cancel];
    for (NSOperation *dep in op.dependencies)
    {
        [self recursiveCancel:op];
    }
}
Matt Fenwick
  • 48,199
  • 22
  • 128
  • 192

2 Answers2

10

There does not exist a notion of an NSOperation automatically cancelling its dependencies. This is because multiple NSOperations may be dependent on the same other NSOperation. The dependency relationship exists strictly to delay execution of a particular NSOperation until all of its dependency NSOperations are complete.

You may consider writing a subclass of NSOperation:

@interface NSOperationOneToOne : NSOperation
- (void)addOneToOneDependency:(NSOperation *)operation;
- (void)removeOneToOneDependency:(NSOperation *)operation;
@end

@implementation NSOperationOneToOne {
  NSMutableArray *oneToOneDependencies;
}
- (void)addOneToOneDependency:(NSOperation *)operation {
  [oneToOneDependencies addObject:operation];
  [self addDependency:operation];
}
- (void)removeOneToOneDependency:(NSOperation *)operation {
  [oneToOneDependencies removeObject:operation];
  [self removeDependency:operation];
}
- (void)cancel {
  [super cancel];
  [oneToOneDependencies makeObjectsPerformSelector:@selector(cancel)];
}
@end

Note: The above code is not guaranteed to be bug-free.

Ian MacDonald
  • 13,472
  • 2
  • 30
  • 51
  • Gotcha, +1. I might try to ensure that all `NSOperation`s from the same dependency graph get added to the same `NSOperationQueue` (and ensure that nothing else is in that queue) so that I can just call `[queue cancelAllOperations]`. – Matt Fenwick Jan 21 '15 at 14:56
  • 1
    If you created a new `NSOperationQueue` for each group of `NSOperation`s that you want to cancel at once, you can use the `underlyingQueue` property to put them on the same system queue. The `NSOperationQueue` will still only cancel the operations it knows about, even if it's running on the same `dispatch_queue_t` as another. – Ian MacDonald Jan 21 '15 at 15:11
  • 1
    From the NSOperationQueue docs: Canceling an operation causes the operation to ignore any dependencies it may have. This behavior makes it possible for the queue to execute the operation’s start method as soon as possible. The start method, in turn, moves the operation to the finished state so that it can be removed from the queue. However this doesn't appear to work. – Brett May 25 '15 at 08:41
2

Sounds to me like you are trying to group operations together in the same queue. To achieve that it's best to split them up using a queue per group. So for each group create an NSOperation subclass in concurrent mode, include a queue, add each sub-operation to the queue. Override cancel and call [super cancel] then [self.queue cancelAllOperations].

A huge advantage of this approach is you can retry operations by adding again to the sub-queue, without affecting the order of the main queue.

malhal
  • 26,330
  • 7
  • 115
  • 133