striker
striker

Reputation: 1263

NSOperationQueue inside NSOperation cause app freeze with waitUntilFinished:YES

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

Upvotes: 3

Views: 913

Answers (1)

Rob
Rob

Reputation: 438232

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.

Upvotes: 4

Related Questions