【问题标题】:NSOperationQueue - Getting Completion Call Too EarlyNSOperationQueue - 过早获得完成调用
【发布时间】:2012-12-17 01:46:33
【问题描述】:

我正在使用 NSOperationQueue 来排队和调用一些地理编码位置查找。我想在所有异步运行的查找完成后调用完成方法。

-(void)geocodeAllItems {

    NSOperationQueue *geoCodeQueue = [[NSOperationQueue alloc]init];
    [geoCodeQueue setName:@"Geocode Queue"];

    for (EventItem *item in [[EventItemStore sharedStore] allItems]) {
        if (item.eventLocationCLLocation){
            NSLog(@"-Location Saved already. Skipping-");
            continue;
        }

        [geoCodeQueue addOperationWithBlock:^{

            NSLog(@"-Geocode Item-");
            CLGeocoder* geocoder = [[CLGeocoder alloc] init];
            [self geocodeItem:item withGeocoder:geocoder];

        }];
    }

    [geoCodeQueue addOperationWithBlock:^{
        [[NSOperationQueue mainQueue]addOperationWithBlock:^{
            NSLog(@"-End Of Queue Reached!-");
        }];
    }];


}

- (void)geocodeItem:(EventItem *)item withGeocoder:(CLGeocoder *)thisGeocoder{

    NSLog(@"-Called Geocode Item-");
    [thisGeocoder geocodeAddressString:item.eventLocationGeoQuery completionHandler:^(NSArray *placemarks, NSError *error) {
        if (error) {
            NSLog(@"Error: geocoding failed for item %@: %@", item, error);
        } else {

            if (placemarks.count == 0) {
                NSLog(@"Error: geocoding found no placemarks for item %@", item);
            } else {
                if (placemarks.count > 1) {
                    NSLog(@"warning: geocoding found %u placemarks for item %@: using the first",placemarks.count,item);
                }
                NSLog(@"-Found Location. Save it-");
                CLPlacemark* placemark = placemarks[0];
                item.eventLocationCLLocation = placemark.location;
                [[EventItemStore sharedStore] saveItems];
            }
        }
    }];
}

输出

[6880:540b] -Geocode Item-
[6880:110b] -Geocode Item-
[6880:540b] -Called Geocode Item-
[6880:110b] -Called Geocode Item-
[6880:110b] -Geocode Item-
[6880:540b] -Geocode Item-
[6880:110b] -Called Geocode Item-
[6880:540b] -Called Geocode Item-
[6880:110b] -Geocode Item-
[6880:580b] -Geocode Item-
[6880:1603] -Geocode Item-
[6880:110b] -Called Geocode Item-
[6880:1603] -Called Geocode Item-
[6880:580b] -Called Geocode Item-
[6880:907] -End Of Queue Reached!-
[6880:907] -Found Location. Save it-
[6880:907] -Found Location. Save it-
[6880:907] -Found Location. Save it-
[6880:907] -Found Location. Save it-
[6880:907] -Found Location. Save it-
[6880:907] -Found Location. Save it-
[6880:907] -Found Location. Save it-

如您所见,End of Queue 函数在所有地理编码过程 + 保存事件的实际结束之前被调用。只有在处理完所有排队查找时,才应在最后显示“已到达队列结束”。我怎样才能把它变成正确的顺序?

【问题讨论】:

  • -Called Geocode Item- 来自哪里?我在代码中没有看到它。
  • 您可以在队列中添加观察者。一个好的答案在这里stackoverflow.com/questions/1049001/…
  • 我更新了代码。我在发布之前做了一个快速清理并错过了那部分。
  • @BerndPlontsch 我不知道你的最终解决方案是什么,但我最终得出的结论是,如果我想使用NSOperationQueue,每个地理编码请求需要两个操作,一个用于初始请求,一个用于完成处理程序,我使每个请求都依赖于前一个请求的完成处理程序。见stackoverflow.com/questions/14195706/…

标签: objective-c ios objective-c-blocks nsoperationqueue


【解决方案1】:

NSOperationQueue 并没有你想的那样工作,执行顺序和添加顺序之间没有直接的依赖关系。你可以调用你减去的函数直到数字等于0,你可以调用“回调”函数。

【讨论】:

  • 我不确定您添加它们的执行顺序与它们的执行顺序之间是否存在没有相关性。它们总是按顺序启动,但由于并发性,当最后一个最终启动时,可能仍有一些完成。如果maxConcurrentOperationCount1(即它是一个串行队列),它将按照 OP 的预期运行。但 Nikolai 提出了一个优雅的解决方案来解决并发队列中的队列操作之间的依赖关系。
【解决方案2】:

一个好的解决方案是将所有地理编码操作添加为最终清理操作的依赖项:

- (void)geocodeAllItems {
    NSOperationQueue *geoCodeQueue = [[NSOperationQueue alloc] init];

    NSOperation *finishOperation = [NSBlockOperation blockOperationWithBlock:^{
        // ...
    }];

    for (EventItem *item in [[EventItemStore sharedStore] allItems]) {
        // ...
        NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
            // ...
        }];
        [finishOperation addDependency:operation]
        [geoCodeQueue addOperation:operation];
    }

    [geoCodeQueue addOperation:finishOperation];
}

另一个解决方案是使操作队列串行。这些操作仍然在后台线程上执行,但一次只执行一个,并且按照它们添加到队列中的顺序:

NSOperationQueue *geoCodeQueue = [[NSOperationQueue alloc] init];
[geoCodeQueue setMaxConcurrentOperationCount:1];

【讨论】:

  • 这对我来说看起来不错。但是,我仍然过早地收到“到达队列结束”消息 - 在“找到位置。保存它”消息之前。可能是在另一个块中发生的实际地理编码存在问题,因此逃离了 NSOperationQueue 的控制范围?
  • @BerndPlontsch 是的,它绝对会导致这个问题,因为CLGeocoder 本身是异步操作的。使用这种依赖技巧时,依赖操作必须单独同步操作。您必须跟踪待处理的geocodeAddressString 请求,因此它必须 (a) 等待 Nikolai 的 completionOperation 触发(意味着所有请求都已提交);然后才 (b) 等待查看是否所有请求都已完成。
【解决方案3】:

NSOperationQueues 默认同时运行多个操作。当然,在实践中,这意味着添加到队列中的操作不一定会按照您添加它们的顺序开始或完成。

您可以在创建队列后通过将队列的 maxConcurrentOperationCount 值设置为 1 来让队列依次运行所有操作:

NSOperationQueue *geoCodeQueue = [[NSOperationQueue alloc]init];
[geoCodeQueue setName:@"Geocode Queue"];
[geoCodeQueue setMaxConcurrentOperationCount:1];

如果您确实希望操作同时运行,但仍希望在它们全部完成时收到通知,请观察队列的 operations 属性并等待它达到零,正如 Srikanth 在他的评论。

编辑:Nikolai Ruhe 的回答也很棒。

【讨论】:

  • +1 指出串行队列将按 OP 预期运行。但是对于并发队列,我总是使用 Nikolai 的方法。它享有更高的并发性,但尊重 OP 所需的依赖关系。为你们俩 +1。
【解决方案4】:

CompletionBlocks 内置在 NSOperation 中,并且 NSBlockOperation 可以处理多个块,因此只需添加运行异步所需的所有工作并将完成块设置为在全部完成时调用,这真的很容易。

- (void)geocodeAllItems {
    NSOperationQueue *geoCodeQueue = [[NSOperationQueue alloc] init];

    NSBlockOperation *operation = [[[NSBlockOperation alloc] init] autorelease]

    for (EventItem *item in [[EventItemStore sharedStore] allItems]) {
        // ...
        // NSBlockOperation can handle multiple execution blocks
        operation addExecutionBlock:^{
            // ... item ...
        }];
    }

    operation addCompletionBlock:^{
         // completion code goes here
         // make sure it notifies the main thread if need be.
    }];

    // drop the whole NSBlockOperation you just created onto your queue
    [geoCodeQueue addOperation:operation];
}

注意:您不能假设这些操作将在您的 geoCodeQueue 中执行。它们将同时运行。 NSBlockOperation 管理这个并发。

【讨论】:

  • 这就像将单个操作添加到串行队列中:所有操作都按顺序执行,而不是同时执行。
  • 哦,我明白了,我错误地假设块是串行执行的。谢谢你教我一些新东西! +1
  • 我不得不承认,我和 Nikolai 一样对这项技术持怀疑态度,所以我只是对其进行了测试。首先,我认为addCompletionBlock 应该是setCompletionBlock。其次,更重要的是,与向队列中添加不同的 NSOperationBlock 对象不同,这似乎不尊重队列的 maxConcurrentOperationCount。当我将maxConcurrentOperationCount 设置为46 时,它在添加不同的NSOperationBlock 对象时会像宣传的那样工作,但是当我只是将addExecutionBlock 设置为单个NSBlockOperation 时,它似乎只能同时运行两个。很好奇。
  • @Rob 我同意。我进行了相同的测试以确保块同时运行。即使在串行队列 (maxConcurrentOperationCount = 1) 上,块也是并行执行的。
【解决方案5】:

这里出现了几个问题。一方面,geocodeAddressString: 是异步的,所以它会立即返回并且块操作正在结束,从而允许下一个操作立即开始。其次,您不应该一个接一个地多次调用geocodeAddressString:。来自 Apple 的有关此方法的文档:

After initiating a forward-geocoding request, do not attempt to 
initiate another forward-or reverse-geocoding request.

第三,您还没有在 NSOperationQueue 上设置最大并发操作数,因此多个块可能会同时执行。

出于所有这些原因,您可能需要使用一些 GCD 工具来跟踪您对 geocodeAddressString: 的调用。您可以使用 dispatch_semaphore (以确保一个在另一个开始之前完成)和一个 dispatch_group (以确保您知道它们何时都完成)来做到这一点 - 类似于以下内容。假设您已经声明了这些属性:

@property (nonatomic, strong) NSOperationQueue * geocodeQueue;
@property (nonatomic, strong) dispatch_group_t geocodeDispatchGroup;
@property (nonatomic, strong) dispatch_semaphore_t geocodingLock;

并像这样初始化它们:

self.geocodeQueue = [[NSOperationQueue alloc] init];
[self.geocodeQueue setMaxConcurrentOperationCount: 1];
self.geocodeDispatchGroup = dispatch_group_create();
self.geocodingLock = dispatch_semaphore_create(1);

您可以像这样进行地理编码循环(我稍微修改了代码以使关键部分更加明显):

-(void) geocodeAllItems: (id) sender
{
    for (NSString * addr in @[ @"XXX Address 1 XXX", @"XXX Address 2 XXX", @"XXX Address 3 XXXX"]) {
        dispatch_group_enter(self.geocodeDispatchGroup);
        [self.geocodeQueue addOperationWithBlock:^{
            NSLog(@"-Geocode Item-");
            dispatch_semaphore_wait(self.geocodingLock, DISPATCH_TIME_FOREVER);
            [self geocodeItem: addr withGeocoder: self.geocoder];
        }];
    }
    dispatch_group_notify(self.geocodeDispatchGroup, dispatch_get_main_queue(), ^{
        NSLog(@"- Geocoding done --");
    });
}

- (void)geocodeItem:(NSString *) address withGeocoder:(CLGeocoder *)thisGeocoder{

    NSLog(@"-Called Geocode Item-");
    [thisGeocoder geocodeAddressString: address completionHandler:^(NSArray *placemarks, NSError *error) {
        if (error) {
            NSLog(@"Error: geocoding failed for item %@: %@", address, error);
        } else {
            if (placemarks.count == 0) {
                NSLog(@"Error: geocoding found no placemarks for item %@", address);
            } else {
                if (placemarks.count > 1) {
                    NSLog(@"warning: geocoding found %u placemarks for item %@: using the first",placemarks.count, address);
                }
                NSLog(@"-Found Location. Save it:");
            }
        }
        dispatch_group_leave(self.geocodeDispatchGroup);
        dispatch_semaphore_signal(self.geocodingLock);
    }];
}

【讨论】:

  • 如果,正如您所说,一个人不应该同时进行并发地理编码请求,这难道不是一个更简单的解决方案吗?为什么不只拥有一个需要地理编码的可变位置数组,并让geocodeItem 抓住第一个项目,将其从数组中删除,对其进行地理编码,然后在其完成块中,将下一个地理编码请求(如果有)排队,并且如果没有,你知道他们都完成了吗?
  • 好点!实际上,这是在强制执行串行实现——真正串行的,因为它等待完成块,不像简单的 NSOperationQueue——这肯定可以用其他不太复杂的方式来完成。我确实认为串行方面很重要,但是它已经实现了;当调用同时启动时,我的测试代码不起作用,但这段代码运行良好。
  • @Rob 你把它钉在那儿了。我检查了文档,彼得是正确的,一次只有一个。真正的解决方案很简单。
猜你喜欢
  • 1970-01-01
  • 2015-03-11
  • 2011-02-24
  • 2013-01-18
  • 2015-03-15
  • 2018-04-27
  • 1970-01-01
  • 2014-01-08
  • 1970-01-01
相关资源
最近更新 更多