NSOperation and NSOperationQueue callback - iphone

I have a class. Inside this class I pass an NSOperation into the NSOperationQueue that lives in my global variables.
Now my NSOperation is finished. I just want to know that it is finished in my class and to have the operation pass data to that class. How is this typically done?

I use the delegate pattern - that was the approach recommended to me by the guys at an Apple Developer Conference.
The scaffolding:
Set up a protocol MyOperationDelegate with a setResult:(MyResultObject *) result method.
Have whoever needs the result implement that protocol.
Add #property id<MyOperationDelegate> delegate; to the NSOperation subclass you've created.
The work:
When you create your operation, but before you queue it, tell it who should receive the result. Often, this is the object that creates the operation: [myOperation setDelegate: self];
At the end of your operation's main function, call [delegate setResult: myResultObject]; to pass the result on.

Another alternative... if your need to do some work when the operation is complete, you could wrap that work up in a block and use a dependancy. This is very simple, especially with NSBlockOperation.
NSOperationQueue* myQueue = [[NSOperationQueue alloc] init];
NSBlockOperation* myOp = [NSBlockOperation blockOperationWithBlock:^{
// Do work
}];
NSBlockOperation* myOp2 = [NSBlockOperation blockOperationWithBlock:^{
// Do work
}];
// My Op2 will only start when myOp is complete
[myOp2 addDependency:myOp];
[myQueue addOperation:myOp];
[myQueue addOperation:myOp2];
Also you can use setCompletionBlock
[myOp setCompletionBlock:^{
NSLog(#"MyOp completed");
}];

Add an observer to you class that listens for changes to the isFinished value of the NSOperation subclass
[operation addObserver:self
forKeyPath:#"isFinished"
options:NSKeyValueObservingOptionNew
context:SOME_CONTEXT];
Then implement the following method, having it look for the context your registered as your listener. You can make the data you want to retrieve from the NSOperation subclass available via an accessor method/property.
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
Check out KVO Programming and Concurrency Programming.
Also, note that the observer will be received on the same thread as the Operation, so you may need to run code on the main thread if you want to deal with the UI.

Related

Dispatch queues, concurrency and completion handling

I have an array of objects to be processed. The objects have a method like below
#interface CustomObject : NSObject
- (void)processWithCompletionBlock:(void (^)(BOOL success))completionBlock;
#end
The processing of each object takes various time and can have different results. And it is known that the processing itself is executing concurrently. To say the truth it would be great to limit the number of concurrent operations because they are pretty intensive.
So I need to enumerate this array of objects and process them. If some object processing fails I need to skip all the rest objects. And of course I need to be notified after all objects will be enumerated and processed.
Should it be solved by the creation of NSOperationQueue and NSOperation subclass? How this class could look to fulfill these requirements? Are there some other elegant approaches?
This is exactly what NSOperation is designed for. Dispatch queues are much lower-level handlers, and you'd have to construct many of the pieces you need for this. You can of course do that (NSOperationQueue is built on top of GCD), but you'd be reinventing NSOperation.
You can handle NSOperation two ways for the most part. If it's simple, you can just create an NSBlockOperation. If it's a bit more complex, you can subclass NSOperation and override the main method to do what you want.
There are several ways to cancel all the other operations. You could have a separate operation queue per group. Then you can easily call cancelAllOperations to shut down everything. Or you could have a separate controller that knows the list of related operations and it could call cancel on them.
Remember that "cancel" just means "don't schedule if it hasn't stared, and set isCancelled if it has." It doesn't abort a running operation. If you want to abort a running operation, the operation needs to periodically check isCancelled.
You typically should limit the number of concurrent operations the queue will run. Use setMaximimumConcurrentOperationCount:.
There are two ways to determine that all the operations are finished. You can make an extra operation (usually a BlockOperation) and use addDependency: to make it depend on all the other operations. That's a nice asynchronous solution. If you can handle a synchronous solution, then you can use waitUntilAllOperationsAreFinished. I typically prefer the former.
Use NSOperationQueue and make your Class an NSOperation
Use this method to queue your work
- (void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait
Add a reference to the operation queue to the NSOperation subclass you create
If an error occurs call
- (void)setSuspended:(BOOL)suspend
on NSOperationQueue
Ok. To help others to understand how this approach can be handled I am sharing my own code.
To limit the number of concurrent threads we can call the -(void)setMaximimumConcurrentOperationCount: method of NSOperationQueue instance.
To iterate objects and provide the completion mechanism we can define the following method:
- (void)enumerateObjects:(NSArray *)objects
{
// define the completion block
NSBlockOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(#"Update UI");
}];
// register dependencies
for (CustomObject *obj in objects) {
CustomOperation *operation = [[CustomOperation alloc] initWithCustomObject:obj];
[completionOperation addDependency:operation]; // set dependencies for the completion block
[_operationQueue addOperation:operation];
}
// register completionOperation on main queue to avoid the cancellation
[[NSOperationQueue mainQueue] addOperation:completionOperation];
}
Overwrite the - (void)start method of the NSOperation subclass to start our custom operation:
- (void)start
{
// We need access to the operation queue for canceling other operations if the process fails
_operationQueue = [NSOperationQueue currentQueue];
if ([self isCancelled]) {
// Must move the operation to the finished state if it is canceled.
[self willChangeValueForKey:#"isFinished"];
_finished = YES;
[self didChangeValueForKey:#"isFinished"];
return;
}
[self willChangeValueForKey:#"isExecuting"];
// We do not need thread detaching because our `-(void)processWithCompletionBlock:` method already uses dispatch_async
[self main]; // [NSThread detachNewThreadSelector:#selector(main) toTarget:self withObject:nil];
_executing = YES;
[self didChangeValueForKey:#"isExecuting"];
}
Overwrite the - (void)main method of the NSOperation subclass to process our custom object:
- (void)main
{
#try {
NSLog(#"Processing object %#", _customObject);
[_customObject processWithCompletionBlock:^(BOOL success) {
_processed = success;
if (!success) {
NSLog(#"Cancelling other operations");
[_operationQueue cancelAllOperations];
}
[self completeOperation];
}];
}
#catch (NSException *exception) {
NSLog(#"Exception raised, %#", exception);
}
}
Thanx to #Rob for pointing me out to the missing part.

Removing observers for an NSOperation

I have a view which loads data via an NSOperation within an NSOperationQueue. I want to allow users to leave this view before the operation has completed. My problem is that I can't seem to consistently do this without crashing. Here is my code to start the operation:
NSOperationQueue* tmpQueue = [[NSOperationQueue alloc] init];
self.queue = tmpQueue;
[tmpQueue release];
SportsLoadOperation* loadOperation = [[SportsLoadOperation alloc] init];
[loadOperation addObserver:self forKeyPath:#"isFinished" options:0 context:NULL];
[self.queue addOperation:loadOperation];
[loadOperation release];
If I leave the view while the operation is still executing, I often get this error:
[SportsViewController retain]: message sent to deallocated instance 0x38b5a0
If I try to remove the observers so that this doesn't occur, like this:
-(void)viewWillDisappear:(BOOL)animated {
if (self.isLoadingData) {
for (NSOperation *operation in [self.queue operations]) {
if([operation isExecuting]) {
[operation cancel];
[operation removeObserver:self forKeyPath:#"isFinished"];
}
}
}
[super viewWillDisappear:animated];
}
Then I sometimes get this error:
Terminating app due to uncaught exception 'NSRangeException', reason:
'Cannot remove an observer <SportsViewController 0x661c730> for the key path "isFinished" from <SportsLoadOperation 0x66201a0> because it is not registered as an observer.'
How can I avoid these problems?
The 2nd error message says it all.
Have you tried to not removeObserver after [operation cancel] and see what happens then?
Have you tried to first removeObserver and only then cancel the operation?
These might help to narrow down the conditions that trigger the error. Also, you might want to add log output to the code to see when it actually executes.
And, like freespace's answer says, adding & removing observers is best done in the construction / destruction methods of the observed instances. This generally yields more stable code.
Looks like you have an instance of SportsLoadOperation that doesn't have SportsViewController as an observer. Are you inserting SportsLoadOperation anywhere else in your code? If this is the case, consider writing an initWithObserver method for SportsLoadOperaion that will do the observing automatically. This avoids errors caused by forgetting to set the observer on isFinished.
Also, it is probably better to do the removal of observer in dealloc then in viewWillDisappear because viewWillDisappear is called in many circumstances, e.g. when displaying a modal view controller. Thus you might be prematurely stopping your operations.
Edit
Instead of checking against [operation isExecuting] check against [operation isCancelled]. This is because [operation cancel] doesn't stop an operation - it can and will continue executing if you don't actually check for isCancelled in your main method. This means that if viewWillDisappear is called twice, you could end up attempting to call removeObserver twice on the same instance of SportsLoadOperation, with the second attempt failing.
Add some debugging output statements in the following places:
when you create a SportsLoadOperation instance and insert it into the queueu
when you are cancelling a SportsLoadOperation instance and removing from it observers.
I ended up solving this by overriding the observed operation's addObserver and removeObserver methods, to keep track of observers so I could remove them in the [cancel] method.
All I have to do now is call the operation queue to cancel all operations before I dismiss the controller that was observing the operations.
Below, _observers is an NSMutableDictionary.
- (void)addObserver:(NSObject*)observer
forKeyPath:(NSString*)keyPath
options:(NSKeyValueObservingOptions)options context:(void*)context
{
[super addObserver:observer forKeyPath:keyPath options:options context:context];
[_observers setValue:observer forKey:keyPath];
}
- (void)removeObserver:(NSObject*)observer forKeyPath:(NSString*)keyPath
{
[super removeObserver:observer forKeyPath:keyPath];
[_observers removeObjectForKey:keyPath];
}
- (void)cancel
{
[super cancel];
for(id key in _observers)
{
id object = [_observers valueForKey:key];
[super removeObserver:object forKeyPath:key];
}
[_observers removeAllObjects];
}

Queueing and Handling multiple requests on iphone

In my application, I'm using NSOperationQueue and NSInvocationOperation objects to execute all the operations. I have multiple queues in my application and I am using KVO "isFinished" on call of which I am performing the next operation on main thread.
The problem is: whenever I perform two operations one after another my application crashes saying:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '<myViewController: 0x481b200>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
Key path: isFinished
Observed object: <NSInvocationOperation: 0x45d9ea0>
Change: {
kind = 1;
}
Context: 0x0'
My general code is as follows:
operation = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(somemethod) object:nil];
[operation addObserver:self forKeyPath:#"isFinished" options:0 context:nil];
[operationQueue addOperation:operation];
[operation release];
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if([keyPath isEqual:#"isFinished"] && operation == object)
{
[operation removeObserver:self forKeyPath:#"isFinished"];
[self performSelectorOnMainThread:#selector(newerPostLoadingNumberOfUdates) withObject:nil waitUntilDone:YES];
}
else
{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
I want to know wheather what is the best practice to enqueue operations that are requested and execute them accordingly?
Thanx in advance.
This will be difficult to answer w/o seeing more of your code as you mention using multiple queues. I see a few things to consider:
1) you have [operation release] and then you compare operation to object in your KVO handler. you should not release `operation until you are done with it. In the KVO handler in this case.
2) as mentioned above, you should do string compares using isEqualToString
3) it sounds like you are trying to run your queue sequentially such that only one operation is executed at a time and you queue another operation after the first has finished. Instead you could set your queue to only perform one operation at a time with:
operationQueue.maxConcurrentOperationCount = 1;
and then add all the operations at once. If order of execution matters, you can use Grand Central Dispatch directly and use dispatch_sync to execute blocks in a specific sequence on a queue.

NSOperation KVO problem

I'm using following function to get my application notified after the operation in nsoperationqueue has finished, so that I can schedule the task that's dependent upon the result of the operation. I'm using:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if([keyPath isEqual:#"isFinished"] && _operation == object)
{
NSLog(#"Our Thread Finished!");
[_operation removeObserver:self forKeyPath:#"isFinished"];
[self performSelectorOnMainThread:#selector(showDialog) withObject:nil waitUntilDone:YES];
}
}
My question is since mostly the tasks assigned to these operations are parsing of data if I try to tap some other button or basically do something that results in action, I get the following exception:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '<Settings: 0x21b970>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
Key path: isFinished
I perfectly understand that since I try doing other things on main thread, because of which the call to main thread:
[self performSelectorOnMainThread:#selector(showDialog) withObject:nil waitUntilDone:YES];
fails to get executed. But what's the solution to this problem as I want both allow user do any action after making a request and also perform the action scheduled after finishing the task assigned to the operation.
Is it really possible?
Thanx in advance.
If you can require Mac OS X 10.6 Snow Leopard, or (I think) iPhone OS 3.0, you can avoid KVO entirely for this. Just create another operation for the work you want to do on the main thread, add the operation it needs to follow as a dependency, and then put the main-thread operation on the main queue:
NSBlockOperration *mainThreadOp = [NSBlockOperation blockOperationWithBlock:^{
[self showDialog];
}];
[mainThreadOp addDependency:backgroundOp];
[[NSOperationQueue mainQueue] addOperation:mainThreadOp];
NSOperation supports dependencies between operations on different queues. Just be careful not to make operations on different queues mutually dependent, because that will result in deadlock.
The reason your are seeing this problem is that you are not using the context pointer.
When you add or remove an observer, pass a context pointer. In your observeValueForKeyPath:ofObject:change:context: , check the context point to make sure the observation being passed belongs to you. If not, call super. It's possible to use self as the context point, or you can take the address of a static string, etc.
Here is an example:
Adding the observer:
[sample addObserver:self forKeyPath:#"finished" options:[self observationOptions] context:(void *)self];
Handling the change notification:
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == (__bridge void *)self){
if ([keyPath isEqualToString:#"finished"]){
// Handle the change here.
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
Removing the observer:
[sample removeObserver:self forKeyPath:#"finished" context:(void *)self];
Because your code was not able to pass one of the notifications to super in observeValueForKeyPath:ofObject:change:context:, that notification went in but never came out. This is why you got that exception.
It unfortunate that many of the Key-Value Observing examples available on The IntarWebs do not do this correctly and/or pass NULL as the context pointer (even Apple's documentation does this).

How to handle setDelegate: when using multipe threads

I've come across an issue with using a third party library, and am not sure what the common pattern to solve it is.
I'm using the asi-http-request class, which fetches http objects asynchronously using a thread.
In my objects dealloc() method, I do
[request setDelegate:nil];
[request release];
However the delegate is sometimes still called after this has happened. (I can see when this happens the delegate field of the request object is nil.) This sometimes causes a crash if the delegate has been destroyed already.
I believe this is a race condition. The code from ASIHTTPRequest that calls the delegate looks like this:
// Let the delegate know we are done
if ([self didFinishSelector] && [[self delegate] respondsToSelector:[self didFinishSelector]]) {
[[self delegate] performSelectorOnMainThread:[self didFinishSelector] withObject:self waitUntilDone:[NSThread isMainThread]];
}
The problem happens if the performerSelectorOnMainThread has been called (but not completed) when the setDelegate call happens on the main thread.
One solution would be to add a wrapper around 'didFinishSelector' that checks (on the main thread) that the delegate is still non-nil before calling the selector, but this would result in a lot of wrappers.
There is some background here:
http://groups.google.com/group/asihttprequest/browse_thread/thread/721220b9645f4a42
All suggestions on the "normal" solution for this appreciated!
Thanks
Joseph
My original thoughts (wrapper around 'didFinishSelector' that checks on the main thread that the delegate is still non-nil before calling the selector) turned out to be the correct solution, as confirmed by helpful folks over on the apple dev forums:
https://devforums.apple.com/message/255935#255935
To avoid my worry of ending up with lots of wrappers, I managed to create only a single wrapper:
- (void)callSelectorCallback:(SEL *)selectorPtr withTarget:(id *)targetPtr
{
id target = *targetPtr;
SEL selector = *selectorPtr;
if (!selector || !target)
return;
if ([target respondsToSelector:selector])
{
[target performSelector:selector withObject:self];
}
}
- (void)callSelector:(SEL *)selector withDelegate:(id *)target
{
if (!*selector || !*target)
return;
SEL callback = #selector(callSelectorCallback:withTarget:);
NSMethodSignature *signature = [ASIHTTPRequest instanceMethodSignatureForSelector:callback];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setSelector:callback];
[invocation setTarget:self];
[invocation setArgument:&selector atIndex:2];
[invocation setArgument:&target atIndex:3];
[invocation performSelectorOnMainThread:#selector(invoke) withObject:nil waitUntilDone:[NSThread isMainThread]];
}
then when I want to call the delegate, the code just looks something like this:
[self callSelector:&didFinishSelector withDelegate:&delegate];
As best I can tell from experiments & code analysis (and assuming setDelegate is only called from the main thread), this is 100% safe. It could be made safe for the non-main thread calls to setDelegate by taking the object lock inside callSelectorCallback.
For dealing with objects across threads, you should almost always retain them. Basically,
id delegate = [[self delegate] retain];
if ([self didFinishSelector] && [delegate respondsToSelector:[self didFinishSelector]]) {
[delegate performSelectorOnMainThread:[self didFinishSelector]
withObject:self
waitUntilDone:[NSThread isMainThread]];
}
[delegate release];
Technically, the delegate could be dealloc-ed in between [self delegate] and the subsequent retain, and I'm not at all sure if Apple's #synthesized atomic accessors protect against this or not, but I believe the only way to solve this is in the accessor,
[[delegate retain] autorelease];
best of luck, race conditions get the best of us!