dependenciesdependency-managementnsoperationnsoperationqueueafhttprequestoperation

NSOperationQueue: a sequence of NSOperation's with dependencies VS (maxConcurrentOperationCount == 1)?


For example I have 3 objects:

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

[op3 addDependency:op2];
[op2 addDependency:op1];

NSOperationQueue *queue = ...;
queue.maxConcurrentOperationCount = 1;
[queue addOperations:@[op1, op2, op3] waitUntilFinished:NO];

I could simply add all the operations in correct order. But for example if op2 is cancelled then I should also cancel op3 and I can't fully clear a queue in this case.

My questions:

1)Is it safe to combine such sequences of operations with maxConcurrentOperationCount == 1?

2)What will the program actually do if I swap around op1 and op2? (op2 should be performed after op1 but the queue is able to take only one from the operations simultaneously)

P.S. In my application I use AFHTTPRequestOperation. Its inheritance hierarchy:

AFHTTPRequestOperation -> AFURLConnectionOperation -> NSOperation

So I can't simply take other subclass of NSOperation.


Solution

  • To answer your questions:

    1. It's safe to combine this particular sequence of operations with the dependencies you have given with maxConcurrentOperations = 1.
    2. The queue will run op2, op3 and op1 or op2, op1, op3 if you reverse the dependency order of op1 and op2.

    Theres nothing tricky in the dependency chain you've specified and NSOperationQueue can take care of things automatically. You can only really get into trouble if you specify a circular dependency (e.g op3 depends on op1), or you have an operation that isn't added to the queue, and so can't get executed to satisfy a dependency.

    Apple has this to say about cancellation in the NSOperationQueue class reference:

    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.

    All NSOperation subclasses should handle cancellation correctly by first checking to see if it has been cancelled and then immediately finish the operation without performing any actions. If this isn't done then it's a bug, and operations may execute even though they have been cancelled.

    (Interestingly this also applies for NSBlockOperation, which I didn't realise. You explicitly need to check self.isCancelled in the block).

    I used CodeRunner on the App Store to try this all out and modified your program slightly. It's reproduced below.

    #import <Foundation/Foundation.h>
    
    int main(int argc, char *argv[]) {
        @autoreleasepool {
            NSOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"op1"); }];
            NSOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"op2"); }];
            NSOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"op3"); }];
    
            [op3 addDependency:op2];
            [op2 addDependency:op1];
    
            NSOperationQueue *queue = [[NSOperationQueue alloc] init];
            queue.maxConcurrentOperationCount = 1;
    
            [queue addOperations:@[op1, op2, op3] waitUntilFinished:YES];
        }
    }
    

    For a NSBlockOperation to refer to itself you need to do this, which is a bit disgusting but looks better in a NSOperation subclass as you can refer to self.

    __block NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"op1 cancelled=%d", op1.cancelled); }];