Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When will completionBlock be called for dependencies in NSOperation

From the docs:

The completion block you provide is executed when the value returned by the isFinished method changes to YES. Thus, this block is executed by the operation object after the operation’s primary task is finished or cancelled.

I'm using RestKit/AFNetworking, if that matters.

I have multiple dependencies in my NSOperation in a OperationQueue. I use the completion block to set some variables (appending the results to an array) that my child requires.

(task1,...,taskN) -> taskA

taskA addDependency: task1-taskN

Will taskA receive incomplete data since the child can execute before the completion block is fired?

Reference

Do NSOperations and their completionBlocks run concurrently?

I did a simple test by adding a sleep in my completion block and I had a different result. The completion block runs in the main thread. While all the completion block are sleeping, the child task ran.

like image 261
Nora Olsen Avatar asked Sep 11 '13 15:09

Nora Olsen


Video Answer


1 Answers

As I discuss below under "a few observations", you have no assurances that this final dependent operation will not start before your other sundry AFNetworking completion blocks have finished. It strikes me that if this final operation really needs to wait for these completion blocks to finish, then you have a couple of alternatives:

  1. Use semaphores within each of the n the completion blocks to signal when they're done and have the completion operation wait for n signals; or

  2. Don't queue this final operation up front, but rather have your completion blocks for the individual uploads keep track of how many pending uploads are still incomplete, and when it falls to zero, then initiate the final "post" operation.

  3. As you pointed out in your comments, you could wrap your invocation of the AFNetworking operation and its completion handler in your own operation, at which point you can then use the standard addDependency mechanism.

  4. You could abandon the addDependency approach (which adds an observer on the isFinished key of the operation upon which this operation is dependent, and once all those dependencies are resolved, performs the isReady KVN; the problem being that this can theoretically happen before your completion block is done) and replace it with your own isReady logic. For example, imagine you had a post operation which you could add your own key dependencies and remove them manually in your completion block, rather than having them removed automatically upon isFinished. Thus, you custom operation

    @interface PostOperation ()
    @property (nonatomic, getter = isReady) BOOL ready;
    @property (nonatomic, strong) NSMutableArray *keys;
    @end
    
    @implementation PostOperation
    
    @synthesize ready = _ready;
    
    - (void)addKeyDependency:(id)key {
        if (!self.keys)
            self.keys = [NSMutableArray arrayWithObject:key];
        else
            [self.keys addObject:key];
    
        self.ready = NO;
    }
    
    - (void)removeKeyDependency:(id)key {
        [self.keys removeObject:key];
    
        if ([self.keys count] == 0)
            self.ready = YES;
    }
    
    - (void)setReady:(BOOL)ready {
        if (ready != _ready) {
            [self willChangeValueForKey:@"isReady"];
            _ready = ready;
            [self didChangeValueForKey:@"isReady"];
        }
    }
    
    - (void)addDependency:(NSOperation *)operation{
        NSAssert(FALSE, @"You should not use addDependency with this custom operation");
    }
    

    Then, your app code could do something like, using addKeyDependency rather than addDependency, and explicitly either removeKeyDependency or cancel in the completion blocks:

    PostOperation *postOperation = [[PostOperation alloc] init];
    
    for (NSInteger i = 0; i < numberOfImages; i++) {
        NSURL *url = ...
        NSURLRequest *request = [NSURLRequest requestWithURL:url];
        NSString *key = [url absoluteString]; // or you could use whatever unique value you want
    
        AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
        [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
            // update your model or do whatever
    
            // now inform the post operation that this operation is done
    
            [postOperation removeKeyDependency:key];
        } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
            // handle the error any way you want
    
            // perhaps you want to cancel the postOperation; you'd either cancel it or remove the dependency
    
            [postOperation cancel];
        }];
        [postOperation addKeyDependency:key];
        [queue addOperation:operation];
    }
    
    [queue addOperation:postOperation];
    

    This is using AFHTTPRequestOperation, and you'd obviously replace all of this logic with the appropriate AFNetworking operation for your upload, but hopefully it illustrates the idea.


Original answer:

A few observations:

  1. As I think you concluded, when your operation completes, it (a) initiates its completion block; (b) makes the queue available for other operations (either operations that had not yet started because of maxConcurrentOperationCount, or because of dependencies between the operations). I do not believe that you have any assurances that the completion block will be done before that next operation commences.

    Empirically, it looks like the dependent operation does not actually trigger until after the completion blocks are done, but (a) I don't see that documented anywhere and (b) this is moot because if you're using AFNetworking's own setCompletionBlockWithSuccess, it ends up dispatching the block asynchronously to the main queue (or the defined successCallbackQueue), thereby thwarting any (undocumented) assurances of synchrony.

  2. Furthermore, you say that the completion block runs in the main thread. If you're talking about the built in NSOperation completion block, you have no such assurances. In fact, the setCompletionBlock documentation says:

    The exact execution context for your completion block is not guaranteed but is typically a secondary thread. Therefore, you should not use this block to do any work that requires a very specific execution context. Instead, you should shunt that work to your application’s main thread or to the specific thread that is capable of doing it. For example, if you have a custom thread for coordinating the completion of the operation, you could use the completion block to ping that thread.

    But if you're talking about one of AFNetworking's custom completion blocks, e.g. those that you might set with AFHTTPRequestOperation's setCompletionBlockWithSuccess, then, yes, it's true that those are generally dispatched back to the main queue. But AFNetworking does this using the standard completionBlock mechanism, so the above concerns still apply.

like image 149
Rob Avatar answered Nov 06 '22 23:11

Rob