NSOperation 依赖陷阱:循环后添加依赖导致过早执行
2025-02-02 23:32:57
NSOperation 依赖管理:在循环后添加的陷阱
当使用 NSOperation
和 NSOperationQueue
执行并发任务时,为任务设置依赖关系是很常见的做法。 然而,在循环中动态添加依赖可能导致一些预期之外的结果,其中一个常见的问题是在循环结束后依赖的任务比预期先执行。本篇文章将分析这个问题,并提供有效的解决方案。
问题依赖设置的“过早”开始
通常,我们的目标是执行一系列独立的任务(例如,从 API 获取数据),然后执行一个汇总任务,汇总任务依赖于所有之前任务的完成。为了实现这个目标,一种常见的方式是在循环中创建并添加到队列的每个任务(使用NSBlockOperation
),然后添加lastOperation
任务并设置循环内所有任务作为lastOperation
的依赖项,最后添加lastOperation
到队列中。
然而,上述代码结构会有一个隐藏的问题: lastOperation
通常会先于或和依赖项并行执行,而不是等到所有循环中的操作都执行完。
这是因为队列会立即开始执行已经加入的操作。即使我们将lastOperation
添加到队列并为其添加依赖项后,队列也会开始执行这个lastOperation
操作。但是由于所有依赖项在循环中逐步添加的,lastOperation
只会在当前循环的最后添加依赖,即添加依赖操作本身并非原子化,存在滞后,它已经尝试运行了。 所以,在所有循环任务都完成之前,lastOperation 运行是合理且正常的。
解决方案一:在循环外设置依赖
一种避免这种 "过早" 执行的有效方法是创建一个用于持有所有子任务操作的数组,并在循环之后设置最终操作的依赖项。具体操作步骤:
- 在循环开始前,初始化一个可变数组
operations
。 - 在循环中,创建子任务
operation
并添加到operations
数组,而不是直接添加到NSOperationQueue
。 - 循环结束后,遍历
operations
数组,为lastOperation
添加依赖项。 - 将所有子任务
operation
添加到队列queue
。 - 将
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, 它也可以用来管理和监控一组并发任务的执行。 具体步骤如下:
- 创建一个
dispatch_group_t
的 group 实例 - 在
dispatch_group_enter()
之前执行NSBlockOperation
任务。 - 在
NSBlockOperation
的 Completion 之后,执行dispatch_group_leave()
。 - 调用
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 的执行模型对实现健壮可靠的代码至关重要。