如何避免多个异步 NSURLSessiond 同时返回的竞争条件
How to avoid race condition with multiple async NSURLSessions returning at the same time
我正在从云端下载数据 API,API 的结构让我下载了多个 "pages" 数据,例如项目 1-100,然后是项目 101- 200等,所以代码流程如下
- 调用 API 告诉我们需要下载多少页
- 使用 NSURLSession
为每个页面发送一个 API 调用
- 每次调用时 returns 都会更新一个计数器,以便我们知道它们何时完成
- 当计数器达到第 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.
}
并且在调用 allTasksFinished 时,检查计数是否匹配,然后重试任何失败的页面加载。
我正在从云端下载数据 API,API 的结构让我下载了多个 "pages" 数据,例如项目 1-100,然后是项目 101- 200等,所以代码流程如下
- 调用 API 告诉我们需要下载多少页
- 使用 NSURLSession 为每个页面发送一个 API 调用
- 每次调用时 returns 都会更新一个计数器,以便我们知道它们何时完成
- 当计数器达到第 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. }
并且在调用 allTasksFinished 时,检查计数是否匹配,然后重试任何失败的页面加载。