NSOperationQueue внутри NSOperation вызывает зависание приложения с помощью waitUntilFinished: YES

У меня есть NSOperation с запросом AFHTTPClient. В конце операции мне нужно выполнить еще N операций с запросами и дождаться завершения запросов, чтобы пометить основную операцию как завершенную.

@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

Ссылка на OBOperation источник в конце вопроса. Это простой класс, который добавляет полезные методы для управления NSOperation потоком.

Пример внутренней операции:

@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

Итак, если я начну свою операцию:

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

Вижу в консоли begin inner operation: 1 и все! Мое приложение полностью зависает (даже пользовательский интерфейс)

После некоторых исследований я решил, что зависание вызвано [queue addOperations:ops waitUntilFinished:YES];. Если я не дождусь завершения, мои внутренние операции будут работать, как и ожидалось, но MyOperation завершится до того, как будут завершены дочерние операции.

Итак, теперь у меня есть обходной путь с операцией зависимого блока:

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];

Но я до сих пор совершенно не понимаю, в чем реальная проблема этой заморозки. Любое объяснение будет очень полезно.

Источник класса OBOperaton: https://dl.dropboxusercontent.com/u/1999619/issue/OBOperation.h https://dl.dropboxusercontent.com/u/1999619/issue/OBOperation.m

Весь проект: https://dl.dropboxusercontent.com/u/1999619/issue/OperationsTest.zip


person striker    schedule 24.05.2014    source источник


Ответы (1)


Причина, по которой вы зашли в тупик, заключается в том, что AFNetworking отправляет блоки завершения в основную очередь. Следовательно, waitUntilFinished в этом первом обработчике success будет блокировать основную очередь до тех пор, пока не закончатся подчиненные запросы. Но эти подчиненные запросы не могут завершиться, потому что им нужно отправить свои блоки завершения в основную очередь, которую все еще блокирует первая операция.

Понятно, что вы все равно никогда не захотите блокировать основную очередь, но вы получите тупиковую ситуацию, если заблокируете основную очередь в ожидании операций, которые сами нуждаются в основной очереди.

person Rob    schedule 24.05.2014
comment
@striker Да, этот обходной путь в порядке. Однако, поскольку вы спрашиваете, я совсем не в восторге от общего дизайна, когда одна операция ожидает других (вы берете одну из maxConcurrentOperationCount с операцией, которая на самом деле ничего не делает, кроме ожидания других). Я предполагаю, что вы делаете это, потому что у вас есть какая-то другая операция, зависящая от завершения всех этих других. Если это так, я бы рассмотрел пользовательский параметр блока completionHandler или что-то в этом роде, а не заставлял первую операцию ждать остальных. - person Rob; 25.05.2014
comment
моя операция на самом деле не просто ждет завершения всех остальных операций. Операция загружает некоторый список с сервера со структурой вроде [{id:1,updatedAt:2014-05-24 19:55},{id:2,updatedAt:2014-05-23 19:55}] и перебирает эти элементы и если updatedAt ‹ время последнего обновления (которое я храню на устройстве), мне нужно выполнить еще один дополнительный запрос, который я инкапсулирую в свою основную операцию. поэтому внутренние операции могут даже не создаваться. - person striker; 25.05.2014