3

I have NSOperation with AFHTTPClient request. In end of operation i need to perform another N operations with requests and wait that requests will be finished to mark main operation as finished

@interface MyOperation : OBOperation

@end

@implementation MyOperation

- (id)init
{
    if (self = [super init]) {
        self.state = OBOperationReadyState;
    }

    return self;
}

- (void)start
{
    self.state = OBOperationExecutingState;

    AFHTTPClient *client = [[AFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:@"http://google.com"]];
    [client getPath:@"/"
         parameters:nil
            success:^(AFHTTPRequestOperation *operation, id responseObject) {
                NSOperationQueue *queue = [NSOperationQueue new];
                queue.maxConcurrentOperationCount = 1;

                NSMutableArray *ops = [NSMutableArray array];
                for (int i = 1; i < 10; i++) {
                    MyInnerOperation *innerOp = [[MyInnerOperation alloc] initWithNumber:@(i)];
                    [ops addObject:innerOp];
                }

                [queue addOperations:ops waitUntilFinished:YES];

                self.state = OBOperationFinishedState;
            } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
                self.state = OBOperationFinishedState;
                NSLog(@"error");
            }];
}

@end

Link to OBOperation source at end of question. It's a simple class that add useful methods to control NSOperation flow

Sample of Inner Operation:

@interface MyInnerOperation : OBOperation

- (id)initWithNumber:(NSNumber *)number;

@end

@implementation MyInnerOperation

- (id)initWithNumber:(NSNumber *)number
{
    if (self = [super init]) {
        _number = number;
        self.state = OBOperationReadyState;
    }

    return self;
}

- (void)start
{
    self.state = OBOperationExecutingState;

    NSLog(@"begin inner operation: %@", _number);

    AFHTTPClient *client = [[AFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:@"http://google.com"]];
    [client getPath:@"/"
         parameters:nil
            success:^(AFHTTPRequestOperation *operation, id responseObject) {
                NSLog(@"inner operation success: %@", _number);
                self.state = OBOperationFinishedState;
            } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
                self.state = OBOperationFinishedState;
                NSLog(@"inner operation error: %@", _number);
            }];
}

@end

So if i begin my operation:

MyOperation *op = [MyOperation new];
[_queue addOperation:op];

I see in console begin inner operation: 1 and that's all! My app totally freeze (even UI)

After some exploration i decide that freeze caused by [queue addOperations:ops waitUntilFinished:YES];. If i don't wait for finish, my inner operations work as expected, but MyOperation finished before child operations will be completed.

So now i have workaround with dependent block operation:

NSBlockOperation *endOperation = [NSBlockOperation blockOperationWithBlock:^{
    self.state = OBOperationFinishedState;
}];

NSMutableArray *ops = [NSMutableArray arrayWithObject:endOperation];
for (int i = 1; i < 10; i++) {
    MyInnerOperation *innerOp = [[MyInnerOperation alloc] initWithNumber:@(i)];
    [ops addObject:innerOp];

    [endOperation addDependency:innerOp];
}

[queue addOperations:ops waitUntilFinished:NO];

But i still totally don't understand what's real problem of this freeze. Any explanation will be very useful.

OBOperaton class source: https://dl.dropboxusercontent.com/u/1999619/issue/OBOperation.h https://dl.dropboxusercontent.com/u/1999619/issue/OBOperation.m

Whole project: https://dl.dropboxusercontent.com/u/1999619/issue/OperationsTest.zip

striker
  • 1,253
  • 3
  • 15
  • 25

1 Answers1

4

The reason you're deadlocking is that AFNetworking dispatches the completion blocks to the main queue. Therefore, waitUntilFinished in that first success handler will block the main queue until the subordinate requests finish. But those subordinate requests cannot finish because they need to dispatch their completion blocks to the main queue, which the first operation is still blocking.

Clearly, you never want to block the main queue anyway, but you receive a deadlock if you block the main queue waiting for operations, which, themselves, need the main queue.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • @striker Yes, that workaround is fine. Since you ask, though, I'm not at all crazy about the overall design, having one operation waiting for others (you're taking up one of the `maxConcurrentOperationCount` with an operation that isn't really doing anything except waiting for others). I presume you're doing that because you have some other operation contingent upon the completion of all of these others. If so, I'd consider a custom `completionHandler` block parameter, or something like that, rather than keeping the first operation waiting for the others. – Rob May 25 '14 at 12:27
  • my operation doesn't really just wait to complete all others operation. Operation upload some list from server with structure like [{id:1,updatedAt:2014-05-24 19:55},{id:2,updatedAt:2014-05-23 19:55}] and iterate through this items and if updatedAt < last updated time (which i store at device) i need to perform one more additional requests which i encapsulate inside my main operation. so inner operations may even don't created. – striker May 25 '14 at 15:55