Get notification when NSOperationQueue finishes all tasks

转自:http://stackoverflow.com/questions/1049001/get-notification-when-nsoperationqueue-finishes-all-tasks


NSOperationQueue has waitUntilAllOperationsAreFinished, but I don't want to wait synchronously for it. I just want to hide progress indicator in UI when queue finishes.

What's the best way to accomplish this?

I can't send notifications from my NSOperations, because I don't know which one is going to be last, and [queue operations] might not be empty yet (or worse - repopulated) when notification is received.

share | improve this question
 

8 Answers

up vote 112 down vote accepted

Use KVO to observe the operations property of your queue, then you can tell if your queue has completed by checking for [queue.operations count] == 0.

When you setup your queue, do this:

[self.queue addObserver:self forKeyPath:@"operations" options:0 context:NULL];

Then do this in your observeValueForKeyPath:

- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object 
                         change:(NSDictionary *)change context:(void *)context
{
    if (object == self.queue && [keyPath isEqualToString:@"operations"]) {
        if ([self.queue.operations count] == 0) {
            // Do something here when your queue has completed
            NSLog(@"queue has completed");
        }
    }
    else {
        [super observeValueForKeyPath:keyPath ofObject:object 
                               change:change context:context];
    }
}

(This is assuming that your NSOperationQueue is in a property named queue)


EDIT: iOS 4.0 now has an NSOperationQueue.operationCount property, which according to the docs is KVO compliant. This answer will still work in iOS 4.0 however, so it's still useful for backwards compatibility.

share | improve this answer
 
 
One quick suggestion: Don't keep calling self.queue. It's needless overhead! Just use 'queue' unless assigning. I know it's just a quick demo block, so don't worry too much about it. :) –   Sam Stewart  Aug 30 '10 at 22:45
20  
I would argue that you should use the property accessor, since it provides future-proofed encapsulation (if you decide e.g. to lazily-initialise the queue). Directly accessing a property by its ivar could be considered premature optimisation, but it really depends on the exact context. The time saved by directly accessing a property through its ivar is usually going to be negligible, unless you are referencing that property more than 100-1000 times a second (as an incredibly crude guesstimate). –   Nick Forge  Aug 31 '10 at 4:17
1  
Tempted to downvote due to bad KVO usage. Proper usage described here:dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage –   Nikolai Ruhe  Jan 2 '13 at 19:03
10  
@NikolaiRuhe You are correct - using this code when subclassing a class which itself uses KVO to observeoperationCount on the same NSOperationQueue object would potentially lead to bugs, in which case you would need to use the context argument properly. It's unlikely to occur, but definitely possible. (Spelling out the actual problem is more helpful than adding snark + a link) –   Nick Forge  Jan 6 '13 at 23:56
3  
Found an interesting idea here. I used that to subclass NSOperationQueue, added an NSOperation property, 'finalOpearation', that is set as a dependent of each operation added to the queue. Obviously had to override addOperation: to do so. Also added a protocol that sends a msg to a delegate when finalOperation completes. Has been working so far. –   pnizzle  May 6 '13 at 7:58 

If you are expecting (or desiring) something that matches this behavior:

t=0 add an operation to the queue.  queueucount increments to 1
t=1 add an operation to the queue.  queueucount increments to 2
t=2 add an operation to the queue.  queueucount increments to 3
t=3 operation completes, queuecount decrements to 2
t=4 operation completes, queuecount decrements to 1
t=5 operation completes, queuecount decrements to 0
<your program gets notified that all operations are completed>

You should be aware that if a number of "short" operations are being added to a queue you may see this behavior instead (because operations are started as part of being added to the queue):

t=0  add an operation to the queue.  queuecount == 1
t=1  operation completes, queuecount decrements to 0
<your program gets notified that all operations are completed>
t=2  add an operation to the queue.  queuecount == 1
t=3  operation completes, queuecount decrements to 0
<your program gets notified that all operations are completed>
t=4  add an operation to the queue.  queuecount == 1
t=5  operation completes, queuecount decrements to 0
<your program gets notified that all operations are completed>

In my project I needed to know when the last operation completed, after a large number of operations had been added to a serial NSOperationQueue (ie, maxConcurrentOperationCount=1) and only when they had all completed.

Googling I found this statement from an Apple developer in response to the question "is a serial NSoperationQueue FIFO?" --

If all operations have the same priority (which is not changed after the operation is added to a queue) and all operations are always - isReady==YES by the time they get put in the operation queue, then a serial NSOperationQueue is FIFO.

Chris Kane Cocoa Frameworks, Apple

In my case it is possible to know when the last operation was added to the queue. So after the last operation is added, I add another operation to the queue, of lower priority, which does nothing but send the notification that the queue had been emptied. Given Apple's statement, this ensures that only a single notice is sent only after all operations have been completed.

If operations are being added in a manner which doesn't allow detecting the last one, (ie, non-deterministic) then I think you have to go with the KVO approaches mentioned above, with additional guard logic added to try to detect if further operations may be added.

:)

share | improve this answer
 
 
Hi, do you know if and how is possible to be notified when each operation in the queue ends by using a NSOperationQueue with maxConcurrentOperationCount=1? –   Cricket  Jun 17 '11 at 9:37
 
@fran: I would have the operations post a notification upon completion. That way other modules can register as observers, and respond as each completes. If your @selector takes a notification object you can easily retrieve the object that posted the notification, in case you need further details about what op just completed. –  software evolved  Jun 19 '11 at 2:30

One alternative is to use GCD. Refer to this as reference.

dispatch_queue_t queue = dispatch_get_global_queue(0,0);
dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group,queue,^{
 NSLog(@"Block 1");
 //run first NSOperation here
});

dispatch_group_async(group,queue,^{
 NSLog(@"Block 2");
 //run second NSOperation here
});

//or from for loop
for (NSOperation *operation in operations)
{
   dispatch_group_async(group,queue,^{
      [operation start];
   });
}

dispatch_group_notify(group,queue,^{
 NSLog(@"Final block");
 //hide progress indicator here
});
share | improve this answer
 

This is how I do it.

Set up the queue, and register for changes in the operations property:

myQueue = [[NSOperationQueue alloc] init];
[myQueue addObserver: self forKeyPath: @"operations" options: NSKeyValueObservingOptionNew context: NULL];

...and the observer (in this case self) implements:

- (void) observeValueForKeyPath:(NSString *) keyPath ofObject:(id) object change:(NSDictionary *) change context:(void *) context {
    if (
        object == myQueue
        &&
        [@"operations" isEqual: keyPath]
    ) {
        NSArray *operations = [change objectForKey:NSKeyValueChangeNewKey];

        if ( [self hasActiveOperations: operations] ) {
            [spinner startAnimating];
        } else {
            [spinner stopAnimating];
        }
    }
}

- (BOOL) hasActiveOperations:(NSArray *) operations {
    for ( id operation in operations ) {
        if ( [operation isExecuting] && ! [operation isCancelled] ) {
            return YES;
        }
    }

    return NO;
}

In this example "spinner" is a UIActivityIndicatorView showing that something is happening. Obviously you can change to suit...

share | improve this answer
 
2  
That for loop seems potentially expensive (what if you cancel all operations at once? Wouldn't that get quadratic performance when queue is being cleaned up?) –   porneL  Apr 18 '10 at 12:59

How about adding an NSOperation that is dependent on all others so it will run last?

share | improve this answer
 
 
It might work, but it's a heavyweight solution, and it would be pain to manage if you need to add new tasks to the queue. –   porneL  Sep 7 '09 at 23:00
 
this is actually very elegant and the one I preferred the most! you my vote. –   yar1vn  Apr 26 '13 at 0:22

What about using KVO to observe the operationCount property of the queue? Then you'd hear about it when the queue went to empty, and also when it stopped being empty. Dealing with the progress indicator might be as simple as just doing something like:

[indicator setHidden:([queue operationCount]==0)]
share | improve this answer
 
 
Did this work for you? In my application the NSOperationQueue from 3.1 complains that it is not KVO-compliant for the key operationCount. –   zoul  Oct 18 '09 at 6:37
 
I didn't actually try this solution in an app, no. Can't say whether the OP did. But the documentation clearly states that it should work. I'd file a bug report. developer.apple.com/iphone/library/documentation/Cocoa/… –  Sixten Otto  Oct 18 '09 at 17:09
 
There is no operationCount property on NSOperationQueue in the iPhone SDK (at least not as of 3.1.3). You must have been looking at the Max OS X documentation page (developer.apple.com/Mac/library/documentation/Cocoa/Reference/…) –   Nick Forge  Apr 17 '10 at 3:49 
 
Time heals all wounds... and sometimes wrong answers. As of iOS 4, the operationCount property is present. –   Sixten Otto  Nov 3 '10 at 14:18

Add the last operation like:

NSInvocationOperation *callbackOperation = [[NSInvocationOperation alloc] initWithTarget:object selector:selector object:nil];

So:

-(void)method: (id)object withSelector:(SEL)selector{
 NSInvocationOperation *callbackOperation = [[NSInvocationOperation alloc] initWithTarget:object selector:selector object:nil];
 [callbackOperation addDependency: ...];
 [operationQueue addOperation:callbackOperation]; 

}
share | improve this answer
 
 
when tasks are executed concurrently then it is wrong approach. –   Marcin  Jun 23 at 9:57

You can create a new NSThread, or execute a selector in background, and wait in there. When the NSOperationQueue finishes, you can send a notification of your own.

I'm thinking on something like:

- (void)someMethod {
    // Queue everything in your operationQueue (instance variable)
    [self performSelectorInBackground:@selector(waitForQueue)];
    // Continue as usual
}

...

- (void)waitForQueue {
    [operationQueue waitUntilAllOperationsAreFinished];
    [[NSNotificationCenter defaultCenter] postNotification:@"queueFinished"];
}
share | improve this answer
 
 
It seems a bit silly to create thread just to put it to sleep. –   porneL  Jun 26 '09 at 20:24
 
I agree. Still, I couldn't find another way around it. –   pgb  Jun 26 '09 at 20:49
 
How would you ensure that only one thread is waiting? I thought about flag, but that needs to be protected against race conditions, and I've ended up using too much NSLock for my taste. –   porneL  Jun 26 '09 at 22:11
 
I think you can wrap the NSOperationQueue in some other object. Whenever you queue an NSOperation, you increment a number and launch a thread. Whenever a thread ends you decrement that number by one. I was thinking on a scenario where you could queue everything beforehand, and then start the queue, so you would need only one waiting thread. –   pgb  Jun 27 '09 at 13:55

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值