返回

NSOperation 依赖陷阱:循环后添加依赖导致过早执行

IOS

NSOperation 依赖管理:在循环后添加的陷阱

当使用 NSOperationNSOperationQueue 执行并发任务时,为任务设置依赖关系是很常见的做法。 然而,在循环中动态添加依赖可能导致一些预期之外的结果,其中一个常见的问题是在循环结束后依赖的任务比预期先执行。本篇文章将分析这个问题,并提供有效的解决方案。

问题依赖设置的“过早”开始

通常,我们的目标是执行一系列独立的任务(例如,从 API 获取数据),然后执行一个汇总任务,汇总任务依赖于所有之前任务的完成。为了实现这个目标,一种常见的方式是在循环中创建并添加到队列的每个任务(使用NSBlockOperation),然后添加lastOperation 任务并设置循环内所有任务作为lastOperation 的依赖项,最后添加lastOperation到队列中。

然而,上述代码结构会有一个隐藏的问题: lastOperation 通常会先于或和依赖项并行执行,而不是等到所有循环中的操作都执行完。

这是因为队列会立即开始执行已经加入的操作。即使我们将lastOperation添加到队列并为其添加依赖项后,队列也会开始执行这个lastOperation操作。但是由于所有依赖项在循环中逐步添加的,lastOperation 只会在当前循环的最后添加依赖,即添加依赖操作本身并非原子化,存在滞后,它已经尝试运行了。 所以,在所有循环任务都完成之前,lastOperation 运行是合理且正常的。

解决方案一:在循环外设置依赖

一种避免这种 "过早" 执行的有效方法是创建一个用于持有所有子任务操作的数组,并在循环之后设置最终操作的依赖项。具体操作步骤:

  1. 在循环开始前,初始化一个可变数组 operations
  2. 在循环中,创建子任务 operation 并添加到operations数组,而不是直接添加到 NSOperationQueue
  3. 循环结束后,遍历 operations 数组,为 lastOperation 添加依赖项。
  4. 将所有子任务 operation 添加到队列 queue
  5. lastOperation添加到队列 queue

代码示例:

[self facebookAccount:^(NSError *error, ACAccount *facebookAccount) {
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue setMaxConcurrentOperationCount:1];
    NSBlockOperation *lastOperation = [NSBlockOperation blockOperationWithBlock:completionAll];
    
    NSMutableArray *operations = [NSMutableArray array];
    for (NSString *postID in postIDs) {
         NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
              NSString *postIDString = [NSString stringWithFormat:@"https://graph.facebook.com/v2.0/%@", postID];
              NSURL *postIDURL = [NSURL URLWithString:postIDString];
             SLRequest *postIDRequest = [SLRequest requestForServiceType:SLServiceTypeFacebook requestMethod:SLRequestMethodGET URL:postIDURL parameters:nil];
             postIDRequest.account = facebookAccount;

                [postIDRequest performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
                    NSError *parseError;
                    NSDictionary *response = [NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingAllowFragments error:&parseError];

                    completion(response);
                }];

        }];
       [operations addObject:operation];
    }
    
    for(NSOperation *op in operations) {
         [lastOperation addDependency:op];
    }
  
    [queue addOperations:operations waitUntilFinished:NO];
     [queue addOperation:lastOperation];

}];

这样,lastOperation 只会在 operations 中所有任务都完成之后执行,因为其依赖项在操作被添加到队列之前都已设置完成。

解决方案二:使用 dispatch groups

另外一个解决方案使用 dispatch group, 它也可以用来管理和监控一组并发任务的执行。 具体步骤如下:

  1. 创建一个dispatch_group_t 的 group 实例
  2. dispatch_group_enter() 之前执行NSBlockOperation任务。
  3. NSBlockOperation 的 Completion 之后,执行 dispatch_group_leave()
  4. 调用dispatch_group_wait() 或者 dispatch_group_notify 来设置任务组完成后最终执行的任务。
    代码如下:
[self facebookAccount:^(NSError *error, ACAccount *facebookAccount) {
   dispatch_group_t group = dispatch_group_create();

   for (NSString *postID in postIDs) {
       dispatch_group_enter(group);
        NSString *postIDString = [NSString stringWithFormat:@"https://graph.facebook.com/v2.0/%@", postID];
        NSURL *postIDURL = [NSURL URLWithString:postIDString];
       SLRequest *postIDRequest = [SLRequest requestForServiceType:SLServiceTypeFacebook requestMethod:SLRequestMethodGET URL:postIDURL parameters:nil];
       postIDRequest.account = facebookAccount;

       [postIDRequest performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
               NSError *parseError;
                NSDictionary *response = [NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingAllowFragments error:&parseError];
                completion(response);
               dispatch_group_leave(group);

        }];


   }
   
     dispatch_group_notify(group, dispatch_get_main_queue(), ^{
           completionAll();
   });
}];

这个方法直接使用了GCD而非 NSOperation,适合更灵活的任务控制,同时它也可以避免之前操作的 过早 执行问题。

注意事项

  • 选择哪种方案取决于你项目的具体需求。第一种方案在需要利用NSOperation本身特性(比如,qualityOfService) 的场景下更适用;第二种方案基于GCD,简洁高效,但是需要引入一个dispatch group, 不适合重度使用NSOperation的场景。
  • 无论选择哪个方案,都应仔细检查操作的完成时机和依赖关系,并进行必要的日志记录。

通过对依赖的正确管理,可以确保 NSOperations 和 dispatch tasks 按照我们期望的顺序执行。 理解 NSOperations 和 dispatch tasks 的执行模型对实现健壮可靠的代码至关重要。