【问题标题】:When will completionBlock be called for dependencies in NSOperation何时会为 NSOperation 中的依赖项调用 completionBlock
【发布时间】:2013-09-11 15:41:50
【问题描述】:

来自文档:

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

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

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

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

taskA addDependency: task1-taskN

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

参考

Do NSOperations and their completionBlocks run concurrently?

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

【问题讨论】:

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


    【解决方案1】:

    正如我在下面的“一些观察”中讨论的那样,您无法保证在其他各种 AFNetworking 完成块完成之前不会开始此最终依赖操作。令我震惊的是,如果这个最终操作确实需要等待这些完成块完成,那么您有两种选择:

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

    2. 不要把这个最后的操作提前排队,而是让你的完成块来跟踪有多少未完成的上传,当它下降到零时,然后启动最后的“发布” "操作。

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

    4. 您可以放弃 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 操作替换所有这些逻辑以进行上传,但希望它说明了这个想法。


    原答案:

    一些观察:

    1. 我认为你的结论是,当你的操作完成时,它 (a) 启动它的完成块; (b) 使队列可用于其他操作(由于maxConcurrentOperationCount 或由于操作之间的依赖关系而尚未开始的操作)。我不相信你有任何保证完成块将在下一次操作开始之前完成。

      根据经验,在完成块完成之前,依赖操作似乎不会真正触发,但是 (a) 我没有看到任何地方记录和 (b) 这没有实际意义,因为如果您使用的是 AFNetworking 自己的setCompletionBlockWithSuccess,它最终将块异步分派到主队列(或定义的 successCallbackQueue),从而阻碍任何(未记录的)同步保证。

    2. 此外,您说完成块在主线程中运行。如果您谈论的是内置的 NSOperation 完成块,则没有这样的保证。其实setCompletionBlockdocumentation says

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

      但是,如果您谈论的是 AFNetworking 的自定义完成块之一,例如那些你可能用AFHTTPRequestOperationsetCompletionBlockWithSuccess 设置的,那么,是的,确实这些通常被分派回主队列。但是 AFNetworking 使用标准的completionBlock 机制来执行此操作,因此上述问题仍然适用。

    【讨论】:

    • 感谢您的建议。我真的不能做#2,因为完成作业是由用户初始化的。对于 #1,最后的任务也是 AFNetworking 工作。我正在努力避免创建自己的自定义类来继承它们,但我确实将它们包装在 NSOperation 类和 start 方法中,在设置 isFinished=True 之前调用它们的 startwaitUntilFinished 方法.这项工作虽然看起来很老套。
    • @NoraOlsen 然后信号量方法使您不必这样做,尽管您将其包装在另一个操作中的方法也有效。两者都觉得很hackish。 :)
    • objective-c中有CountDownLatch吗?
    • @Rob。我也是这么想的。因为我确切地知道我需要依赖的后期操作的数量,所以我确实可以使用类似的东西。
    • @TikhonovAlexander - 完全正确。当 Apple 添加ready 作为实际属性(iOS 8、IIRC)时,您必须显式合成才能获得支持的 ivar。我已经相应地更新了答案。
    【解决方案2】:

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

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

    否则,NSOperation 的完成块的执行上下文是“未定义的”。这意味着,完成块可以在任何线程/队列上执行。事实上,它在一些私有队列上执行。

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


    编辑:

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

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

    更简单的方法是使用在 JavaScript、Python、Scala 和其他一些语言中很常见的“Promises”。

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

    “Promises”(有时称为 Futures 或 Deferred)表示异步任务的最终结果。您的 fetch 请求就是这样的异步任务。但是,异步方法/任务返回一个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:。然后当这成功完成时,它会打印结果,否则它会打印错误。

    依次调用多个异步任务,一个接一个,称为“继续”或“链接”。

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


    您可能想知道如何捕获任何错误,例如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* 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”。你只能一次性解决一个承诺。

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

    - (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 可以“履行”并传递结果值,也可以“拒绝”传递原因(错误)。

    根据实际的实现,一个 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;
    }
    

    注意:您可以根据需要注册成功或失败处理程序,时间、地点和数量。


    因此,您可以使用 Promise 做很多事情 - 甚至比这个简短的介绍更多。如果你读到这里,你可能会知道如何解决你的实际问题。它就在那里 - 只需几行代码。

    我承认,对 Promise 的简短介绍非常粗略,而且对于 Objective-C 开发人员来说也很新,而且听起来可能并不常见。

    您可以在 JS 社区中阅读很多关于 Promise 的内容。 Objective-C 中有一到三个实现。实际实现不会超过几百行代码。碰巧,我是其中之一的作者:

    RXPromise.

    持保留态度,我可能完全有偏见,显然所有其他人也曾处理过 Promise。 ;)

    【讨论】:

    • 我使用的是AFHTTPRequestOperation,实际上是RKRequestOperation,并直接添加到NSOperationQ。两者都有setCompletionBlockWithSuccess:failure,内部是NSOperation completionBlock。由于回调执行时间未定义,因此子进程可以在运行之前执行。我见过诸如raywenderlich.com/19788/… 之类的解决方案,它们使用自己的 NSOperationQ 来保留可以传递的资源。
    • 父级的isFinished属性设置为YES后才会启动依赖Op。这确实可以早于完成块的执行。如果这是一个问题,我们可以制定解决方案。
    • 是的。这就是问题:)
    • 我最初认为这看起来与回调非常相似,只是可能objective-c 缺少延迟回调构造。这对我来说确实很有希望,而且曲线相当陡峭。由于我拥有服务器端,因此我可以稍微更改流程,而不依赖于最终完成作业中“准备”任务中的信息。我试图做一个两阶段的方法:1.照片上传 2.发布他们的内容。最初,后端将返回客户端在第 2 步中一起发送的照片 ID。
    • Promises 和 NSOperationQueues 不会以任何方式发生冲突。事实上,他们构建了一个高效的团队:NSOperationQueues 定义了任务工作负载的执行上下文(应该是任务的实现细节),而 Promises 定义了业务操作的“逻辑”和“控制流”。 Promise 可以随时随地发送“取消”消息,发送次数不限。然而,Promise 只会被解析一次——它第一次收到完成、拒绝或取消信号时。
    猜你喜欢
    • 1970-01-01
    • 2015-03-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-08-15
    • 2023-04-08
    相关资源
    最近更新 更多