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

时间:2013-09-11 15:41:51

标签: objective-c restkit afnetworking objective-c-blocks nsoperationqueue

来自文档:

  

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

我正在使用RestKit/AFNetworking,如果重要的话。

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

(task1,...,taskN) - >任务A

taskA addDependency:task1-taskN

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

参考

Do NSOperations and their completionBlocks run concurrently?

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

2 个答案:

答案 0 :(得分:5)

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

  1. 在每个 n 完成块中使用信号量,以便在完成时发出信号并完成操作,等待 n 信号;或

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

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

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

    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,并在完成块中明确显示addDependencyremoveKeyDependency

    cancel

    这是使用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]; ,您显然会使用适当的AFNetworking操作替换所有这些逻辑,但希望它能说明这一想法。


  5. 原始答案:

    一些观察结果:

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

      根据经验,看起来依赖操作直到完成块完成后才会实际触发,但是(a)我没有在任何地方看到记录,(b)这是没有意义的,因为如果你使用AFNetworking自己的maxConcurrentOperationCount,它最终将块异步调度到主队列(或定义的setCompletionBlockWithSuccess),从而阻止任何(未记录的)同步保证。

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

        

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

      但是,如果您正在谈论AFNetworking的一个自定义完成块,例如那些你可能用setCompletionBlock的{​​{1}}设置的那些,那么,是的,通常会将它们发送回主队列。但AFNetworking使用标准AFHTTPRequestOperation机制来做到这一点,因此上述问题仍然适用。

答案 1 :(得分:2)

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

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

否则,NSOperation完成块的执行上下文是&#34;未定义&#34;。这意味着,完成块可以在任何线程/队列上执行。实际上它在某个私有队列上执行。

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


编辑:

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

另一种方法需要实用程序类或类库,它特别适合以更简洁和简单的方式解决异步问题。 ReactiveCocoa能够解决这个(一个简单的)问题。然而,它过于复杂,它实际上有一个学习曲线&#34; - 而且是陡峭的。我不推荐它,除非你同意花几周时间学习它并且有很多其他异步用例甚至更复杂的用例。

更简单的方法是利用&#34; Promises&#34;这在JavaScript,Python,Scala和其他一些语言中很常见。

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

&#34;承诺&#34; (有时称为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:。然后,当成功完成时,它会打印结果,否则会输出错误。

一个接一个地按顺序调用几个异步任务,称为&#34; continuation&#34;或者&#34;链接&#34;。

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


您可能想知道如何捕获任何错误,例如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。当这个完成后,又可以启动另一个,所以强行。那是&#34;继续&#34; ;)


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

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 最终将成为值@&#34; OK&#34;或NSError。


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

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

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

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

设置承诺的价值将被称为:&#34;解决&#34;。您只能通过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;
}

完成任务后,可以履行承诺&#34;将结果值传递给它,或者它可以被拒绝&#34;传递原因(错误)。

根据实际实施情况,也可以取消承诺。比如说,您持有对请求操作的引用:

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

带着一粒盐,我可能完全有偏见,显然所有其他人都曾经处理过Promises。 ;)