首页 文章

何时将在NSOperation中为completionBlock调用依赖项

提问于
浏览
6

来自文档:

当isFinished方法返回的值更改为YES时,将执行您提供的完成块 . 因此,在操作的主要任务完成或取消之后,操作对象执行该块 .

我正在使用 RestKit/AFNetworking ,如果这很重要的话 .

我在 OperationQueue 中的 NSOperation 中有多个依赖项 . 我使用完成块来设置我的孩子需要的一些变量(将结果附加到数组) .

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

taskA addDependency:task1-taskN

taskA是否会收到不完整的数据,因为孩子可以在完成块被触发之前执行?

Reference

Do NSOperations and their completionBlocks run concurrently?

我通过在完成块中添加一个睡眠进行了简单的测试,结果不一样 . 完成块在主线程中运行 . 当所有完成块都处于休眠状态时,子任务就会运行 .

2 回答

  • 5

    正如我在下面的“一些观察”中所讨论的那样,您无法保证在您的其他各种AFNetworking完成块完成之前,此最终相关操作不会启动 . 令我感到震惊的是,如果这个最终操作真的需要等待这些完成块完成,那么你有几个选择:

    • 在完成块的每一个内使用信号量,以便在完成时发出信号并完成操作等待n个信号;要么

    • 不要预先排队这个最终操作,而是让你的个人上传完成块跟踪有多少待处理的上传仍然不完整,当它下降到零时,然后启动最后的“发布”操作 .

    • 正如您在评论中指出的那样,您可以在自己的操作中包装AFNetworking操作及其完成处理程序的调用,然后您可以使用标准的 addDependency 机制 .

    • 您可以放弃 addDependency 方法(在此操作所依赖的操作的 isFinished 键上添加观察者,并且一旦所有这些依赖关系得到解决,执行 isReady KVN;问题在于理论上这可能在您完成之前发生阻止完成)并用您自己的 isReady 逻辑替换它 . 例如,假设您有一个post操作,您可以添加自己的密钥依赖项并在完成块中手动删除它们,而不是在 isFinished 时自动删除它们 . 这样,你自定义操作

    @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");
    }
    

    然后,您的应用程序代码可以执行类似的操作,使用 addKeyDependency 而不是 addDependency ,并在完成块中显式显示 removeKeyDependencycancel

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

    这是使用 AFHTTPRequestOperation ,您显然会使用适当的AFNetworking操作替换所有这些逻辑,但希望它能说明这个想法 .


    原始答案:

    一些观察:

    • 我认为你的结论是,当你的行动完成时,它(a)启动完成块; (b)使队列可用于其他操作(由于 maxConcurrentOperationCount 而尚未启动的操作,或者由于操作之间的依赖性) . 我不相信您确信在下一次操作开始之前完成块将完成 .

    根据经验,看起来依赖操作直到完成块完成后才会实际触发,但是(a)我没有使用AFNetworking自己的 setCompletionBlockWithSuccess ,它最终将块异步调度到主队列(或定义的 successCallbackQueue )从而阻止任何(无证)同步保证 .

    • 此外,您说完成块在主线程中运行 . 如果你在谈论内置的 NSOperation 完成块,你没有这样的保证 . 事实上, setCompletionBlock documentation says

    无法保证完成块的确切执行上下文,但通常是辅助线程 . 因此,您不应该使用此块来执行任何需要非常特定的执行上下文的工作 . 相反,您应该将该工作分流到应用程序的主线程或能够执行此操作的特定线程 . 例如,如果您有一个用于协调操作完成的自定义线程,则可以使用完成块来ping该线程 .

    但是如果你自定义完成块,例如那些你可能用 AFHTTPRequestOperation 设置的 setCompletionBlockWithSuccess ,然后,是的,这是真的那些通常被派遣回主队列 . 但AFNetworking使用标准 completionBlock 机制来做到这一点,因此上述问题仍然适用 .

  • 2

    如果你的 NSOperation 是AFHTTPRequestOperation的子类,这很重要 . AFHTTPRequestOperation在方法 setCompletionBlockWithSuccess:failure 中将 NSOperation 的属性 completionBlock 用于其自身目的 . 在这种情况下,请不要自己设置属性 completionBlock

    看来,AFHTTPRequestOperation的成功和失败处理程序将在主线程上运行 .

    否则, NSOperation 的完成块的执行上下文是"undefined" . 这意味着,完成块可以在任何线程/队列上执行 . 实际上它在某个私有队列上执行 .

    IMO,这是首选方法,除非执行上下文应由调用站点明确指定 . 在线程或队列上执行完成处理程序可以访问哪些实例(例如主线程)很容易导致不谨慎的开发人员死锁 .


    编辑:

    如果要在完成父操作的完成块之后启动依赖操作,可以通过使完成块内容本身为 NSBlockOperation (新父节点)来解决此问题,并将此操作作为依赖项添加到子操作和在队列中启动它 . 你可能会意识到,这很快变得笨拙 .

    另一种方法需要实用程序类或类库,它特别适合以更简洁和简单的方式解决异步问题 . ReactiveCocoa将能够解决这样一个(一个简单的)问题 . 但是,除非你同意花几周时间学习它并且有很多其他异步用例甚至更复杂的用例,否则它会推荐它 .

    一种更简单的方法将使用“Promises”,这在JavaScript,Python,Scala和一些其他语言中非常常见 .

    现在,请仔细阅读,(简单)解决方案实际上如下:

    "Promises"(有时称为Futures或Deferred)表示异步任务的最终结果 . 您的提取请求是这样的异步任务 . 但是,相反指定完成处理程序,异步方法/任务返回一个Promise:

    -(Promise*) fetchThingsWithURL:(NSURL*)url;
    

    您可以通过注册成功处理程序块或失败处理程序块来获得结果 - 或错误 - 如下所示:

    Promise* thingsPromise = [self fetchThingsWithURL:url];
    thingsPromise.then(successHandlerBlock, failureHandlerBlock);
    

    或者,内联块:

    thingsPromise.then(^id(id things){
       // do something with things
       return <result of success handler>
    }, ^id(NSError* error){
       // Ohps, error occurred
       return <result of failure handler>
    });
    

    而且更短:

    [self fetchThingsWithURL:url]
    .then(^id(id result){
         return [self.parser parseAsync:result];
    }, nil);
    

    这里, parseAsync: 是一个返回Promise的异步方法 . (是的,承诺) .


    您可能想知道如何从解析器中获取结果?

    [self fetchThingsWithURL:url]
    .then(^id(id result){
         return [self.parser parseAsync:result];
    }, nil)
    .then(^id(id parserResult){
        NSLog(@"Parser returned: %@", parserResult);
        return nil;  // result not used
    }, nil);
    

    这实际上启动了异步任务 fetchThingsWithURL: . 然后,当成功完成后,它将启动异步任务 parseAsync: . 然后,当成功完成时,它会打印结果,否则会输出错误 .

    依次调用几个异步任务,一个接一个地称为“continuation”或“chaining” .

    请注意,上面的整个语句是异步的!也就是说,当您将上述语句包装到方法中并执行它时,该方法立即返回 .


    您可能想知道如何捕获任何错误,例如 fetchThingsWithURL: 失败或 parseAsync:

    [self fetchThingsWithURL:url]
    .then(^id(id result){
         return [self.parser parseAsync:result];
    }, nil)
    .then(^id(id parserResult){
        NSLog(@"Parser returned: %@", parserResult);
        return nil;  // result not used
    }, nil)
    .then(/*succes handler ignored*/, ^id (NSError* error){
        // catch any error
        NSLog(@"ERROR: %@", error);
        return nil; // result not used
    });
    

    处理程序在相应的任务完成后执行(当然) . 如果任务成功,将调用成功处理程序(如果有) . 如果任务失败,将调用错误处理程序(如果有) .

    处理程序可以返回Promise(或任何其他对象) . 例如,如果异步任务成功完成,则将调用其成功处理程序,该处理程序将启动另一个异步任务,该任务将返回promise . 当这个完成后,又可以启动另一个,所以强行 . 那是"continuation";)


    您可以从处理程序返回任何内容:

    Promise* finalResult = [self fetchThingsWithURL:url]
    .then(^id(id result){
         return [self.parser parseAsync:result];
    }, nil)
    .then(^id(id parserResult){
        return @"OK";
    }, ^id(NSError* error){
        return error;
    });
    

    现在,finalResult最终将成为值@ "OK"或NSError .


    您可以将最终结果保存到数组中:

    array = @[
        [self task1],
        [self task2],
        [self task3]
    ];
    

    所有任务成功完成后继续:

    [Promise all:array].then(^id(results){
        ...
    }, ^id (NSError* error){
        ...
    });
    

    设置promise的值将被称为:“resolving” . 您只能通过ONCE解决承诺 .

    您可以将带有完成处理程序或完成委托的任何异步方法包装到返回promise的方法中:

    - (Promise*) fetchUserWithURL:(NSURL*)url 
    {
        Promise* promise = [Promise new];
    
        HTTPOperation* op = [[HTTPOperation alloc] initWithRequest:request 
            success:^(NSData* data){
                [promise fulfillWithValue:data];
            } 
            failure:^(NSError* error){
                [promise rejectWithReason:error];
            }];
    
        [op start];
    
        return promise;
    }
    

    完成任务后,可以“履行”承诺,将结果值传递给它,或者可以“拒绝”传递原因(错误) .

    根据实际的实施,也可以取消Promise . 比如说,您持有对请求操作的引用:

    self.fetchUserPromise = [self fetchUsersWithURL:url];
    

    您可以取消异步任务如下:

    - (void) viewWillDisappear:(BOOL)animate {
        [super viewWillDisappear:animate];
        [self.fetchUserPromise cancel];
        self.fetchUserPromise = nil;
    }
    

    要取消关联的异步任务,请在包装器中注册失败处理程序:

    - (Promise*) fetchUserWithURL:(NSURL*)url 
    {
        Promise* promise = [Promise new];
    
        HTTPOperation* op = ... 
        [op start];
    
        promise.then(nil, ^id(NSError* error){
            if (promise.isCancelled) {
                [op cancel];
            }
            return nil; // result unused
        });
    
        return promise;
    }
    

    注意:您可以注册成功或失败处理程序,何时,何地以及根据需要注册 .


    所以,你可以通过承诺做很多事 - 甚至比这个简短的介绍更多 . 如果你读到这里,你可能会想到如何解决你的实际问题 . 它就在那里 - 它是几行代码 .

    我承认,这个对promises的简短介绍非常粗糙,对Objective-C开发人员来说也是一个新手,并且可能听起来不常见 .

    你可以在JS社区中阅读很多关于promises的内容 . Objective-C中有一个或三个实现 . 实际实现不会超过几百行代码 . 它发生了,我是其中一个的作者:

    RXPromise .

    拿一粒盐,我可能完全有偏见,显然所有其他人都曾经处理过Promise . ;)

相关问题