如何避免多个异步 NSURLSessiond 同时返回的竞争条件

How to avoid race condition with multiple async NSURLSessions returning at the same time

我正在从云端下载数据 API,API 的结构让我下载了多个 "pages" 数据,例如项目 1-100,然后是项目 101- 200等,所以代码流程如下

  1. 调用 API 告诉我们需要下载多少页
  2. 使用 NSURLSession
  3. 为每个页面发送一个 API 调用
  4. 每次调用时 returns 都会更新一个计数器,以便我们知道它们何时完成
  5. 当计数器达到第 1 步的答案时,我们向 GUI 发送一条通知,告知所有下载已完成并相应地更新 GUI

我的问题是我最终处于竞争状态,似乎有几个 API 调用同时更新我的​​计数器,因此导致它不正确。

这是我处理更新计数器然后发送通知的代码

+ (void) fcFoundProductNumber:(NSNotification *)notification {
    if([NWTillHelper isDebug] == 1) {
        NSLog(@"%s entered", __PRETTY_FUNCTION__);
    }

    NSMutableArray *fcVariants = [[NSMutableArray alloc] init];

    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];

    int numberOfPages = [[userDefaults objectForKey:@"fcNumberOfPages"] intValue];
    int loopCounter = [[userDefaults objectForKey:@"loopCounter"] intValue] + 1;
    [userDefaults setObject:[NSNumber numberWithInt:loopCounter] forKey:@"loopCounter"];
    [userDefaults synchronize];

    if([NWTillHelper isDebug] == 1) {
        NSLog(@"%s \r\nnumberOfPages = %d\r\nloopCounter = %d", __PRETTY_FUNCTION__, numberOfPages, loopCounter);
    }

    if(numberOfPages == loopCounter) {
        if([NWTillHelper isDebug] == 1) {
            NSLog(@"%s numberOfPages == loopCounter\r\nnumberOfPages = %d\r\nloopCounter = %d", __PRETTY_FUNCTION__, numberOfPages, loopCounter);
        }

        [[NSNotificationCenter defaultCenter] removeObserver:self];
        NSArray *fcProduct = [userDefaults objectForKey:@"fcProduct"];
        [userDefaults setObject:[NSNumber numberWithInt:0] forKey:@"loopCounter"];
        [userDefaults synchronize];

        for(NSDictionary *fcVariant in fcProduct) {
            NSMutableDictionary *fcVariantRow = [[NSMutableDictionary alloc] init];
            NSArray *fcVariantSwatches = fcVariant[@"availableSwatches"];
            NSDictionary *fcVariantSwatch = [fcVariantSwatches objectAtIndex:0];

            NSString *activityArticleNumber = fcVariant[@"activityArticleNumber"];
            NSString *colourDescription = fcVariant[@"colourDescription"];
            NSString *name = fcVariant[@"name"];
            NSString *price = fcVariant[@"priceInfo"][@"price"];
            NSString *onSale = fcVariant[@"priceInfo"][@"onSale"];
            NSString *formattedPrice = fcVariant[@"priceInfo"][@"formattedPrice"];
            NSString *primaryImage = fcVariant[@"primaryImage"][@"url"];
            NSString *stockState = fcVariant[@"stockState"];
            NSString *variantSwatch = fcVariantSwatch[@"fabricUrl"];

            [fcVariantRow setObject:activityArticleNumber forKey:@"fcVariantItemId"];
            [fcVariantRow setObject:colourDescription forKey:@"fcVariantColourDescription"];
            [fcVariantRow setObject:name forKey:@"fcVariantName"];
            [fcVariantRow setObject:price forKey:@"fcVariantPrice"];
            [fcVariantRow setObject:onSale forKey:@"fcVariantOnSale"];
            [fcVariantRow setObject:formattedPrice forKey:@"fcVariantFormattedPrice"];
            [fcVariantRow setObject:primaryImage forKey:@"fcVariantImageUrl"];
            [fcVariantRow setObject:stockState forKey:@"fcVariantStockState"];
            [fcVariantRow setObject:variantSwatch forKey:@"fcVariantSwatch"];

            [fcVariants addObject:fcVariantRow];
        }
        [userDefaults setObject:fcVariants forKey:@"fcVariants"];
        [userDefaults synchronize];
        [[NSNotificationCenter defaultCenter] postNotificationName:@"fcVariantsDone" object:nil];
    }
}

这是说明问题的调试

2017-12-20 19:33:52.504312+0800 NWMPos[29258:18702451] +[FullWebServices fcFoundProductNumber:] entered
2017-12-20 19:33:52.507301+0800 NWMPos[29258:18701949] +[FullWebServices fcFoundProductNumber:] entered
2017-12-20 19:33:52.507203+0800 NWMPos[29258:18701928] +[FullWebServices fcFoundProductNumber:] entered
2017-12-20 19:33:52.509736+0800 NWMPos[29258:18702451] +[FullWebServices fcFoundProductNumber:] 

numberOfPages = 3

loopCounter = 1
2017-12-20 19:33:52.512614+0800 NWMPos[29258:18701949] +[FullWebServices fcFoundProductNumber:] 

numberOfPages = 3

loopCounter = 2
2017-12-20 19:33:52.512614+0800 NWMPos[29258:18701928] +[FullWebServices fcFoundProductNumber:] 

numberOfPages = 3

loopCounter = 2

正如您从前 3 行中看到的那样,我发出了 3 个 API 调用,其中 3 个正在返回,但最后 2 个似乎同时返回,正如您所看到的,这会导致计数器未更新。

如何避免这种竞争情况?

这种方法对我来说听起来很脆弱。以下是一些改进建议(包括修复此错误):

  • 您在任何情况下都不应将 NSUserDefaults 用于经常更改的内容。 NSUserDefaults 将数据写入磁盘。你这样做是在滥用设备上的闪存。
  • 你不应该数数。您应该将状态存储在以页码为键的可变字典中,例如

    @synchronized(self) {
      self.pageDictionary[@(pageNumber)] = pageData;
    }
    

    然后 self.pageDictionary.count 是迄今为止检索到的页数。集合周围的 @synchronize 块是因为 NSMutableDictionary 不是完全异步安全的。您可以将其包装在您的数学中,它 可能 有效,但由于前面提到的原因,它仍然是一个坏主意。

  • 字典方法还有一个好处是,当(而不是如果)网络错误导致其中一些加载失败时,您可以智能地重试。
  • 您可能不需要等待整个文档准备就绪即可使用。如果页面没有准备好,等待它并呈现一个空白页面。您可以通过检查字典来判断页面是否准备就绪。
  • 如果出于某种奇怪的原因确实需要等待一切准备就绪,请使用调度组,例如

    @implementation foo {
      dispatch_group_t _syncGroup;
    }
    
    - (...)init {
      _syncGroup = dispatch_group_create();
    }
    
    - (...)startTask... {
      ...
      dispatch_group_enter(_syncGroup);
      NSURLSessionDataTask *task = [NSURLSessionDataTask dataTaskWithURL:...
                                                       completionHandler:^{
        dispatch_group_leave(_syncGroup);
      }];
      [task resume];
      dispatch_group_notify(_syncGroup, dispatch_get_main_queue(),^{
        [self allTasksFinished];
      });
    }
    
    - (void)allTasksFinished {
      // When we get here, the dispatch group went empty.
    }
    

    并且在调用 allTask​​sFinished 时,检查计数是否匹配,然后重试任何失败的页面加载。