问题
I am using a NSOperationQueue to queue and call a number of Geocoding location lookups. I want to call a completion method when all asynchronicly running lookups have been finished.
-(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];
}
}
}];
}
Output
[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-
As you can see the End of Queue function is called before the actual end of all geocoding processes + saving events. "End of Queue Reached" should only be displayed at the very end when all queued lookups have been processed. How can I get this into the right order?
回答1:
Several issues are coming up here. For one, geocodeAddressString:
is asynchronous, so it is returning immediately and the block operation is ending, allowing the next one to start right away. Second, you should not be making multiple calls to geocodeAddressString:
one right after the other. From Apple's docs for this method:
After initiating a forward-geocoding request, do not attempt to
initiate another forward-or reverse-geocoding request.
Third, you haven't set a max number of concurrent operations on your NSOperationQueue, so multiple blocks may be executing at once anyway.
For all of these reasons, you might want to use some GCD tools to track your calls to geocodeAddressString:
. You could do this with a dispatch_semaphore (to make sure one finishes before the other starts) and a dispatch_group (to make sure you know when all of them have finished) -- something like the following. Let's assume you've declared these properties:
@property (nonatomic, strong) NSOperationQueue * geocodeQueue;
@property (nonatomic, strong) dispatch_group_t geocodeDispatchGroup;
@property (nonatomic, strong) dispatch_semaphore_t geocodingLock;
and initialized them like this:
self.geocodeQueue = [[NSOperationQueue alloc] init];
[self.geocodeQueue setMaxConcurrentOperationCount: 1];
self.geocodeDispatchGroup = dispatch_group_create();
self.geocodingLock = dispatch_semaphore_create(1);
You could do your geocoding loop like this (I've altered the code a bit to make the key parts more obvious):
-(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);
}];
}
回答2:
A good solution is to add all geocoding operations as dependencies of the final cleanup operation:
- (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];
}
Another solution would be to make the operation queue serial. The operations are still performed on a background thread, but only one at a time and in the order in which they are added to the queue:
NSOperationQueue *geoCodeQueue = [[NSOperationQueue alloc] init];
[geoCodeQueue setMaxConcurrentOperationCount:1];
回答3:
CompletionBlocks are built in to NSOperation and NSBlockOperation can handle multiple blocks so it's really easy to just add all the work that you need to run async and set your completion block to be called when it is all finished.
- (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];
}
Note: You can't assume that the operations will be performed in your geoCodeQueue. They will be run concurrently. NSBlockOperation manages this concurrency.
回答4:
NSOperationQueue doesn't work in the way you think, there is no direct dependence between the execution order and adding order. You can call the function in which you subtract till the number equals to zero and you can call the "callback" function.
回答5:
NSOperationQueues run multiple operations concurrently by default. In practice of course, that means that operations added to the queue will not necessarily be started or finished in the same order you added them.
You can make the queue run all operations serially by setting the queue's maxConcurrentOperationCount value to 1 after you create it:
NSOperationQueue *geoCodeQueue = [[NSOperationQueue alloc]init];
[geoCodeQueue setName:@"Geocode Queue"];
[geoCodeQueue setMaxConcurrentOperationCount:1];
If you do indeed want operations to run concurrently, but still want to be notified when they've all finished, observe the queue's operations
property and wait until it reaches zero, as explained in answer linked to by Srikanth in his comment.
EDIT: Nikolai Ruhe's answer is great too.
来源:https://stackoverflow.com/questions/14126350/nsoperationqueue-getting-completion-call-too-early