I have an NSOperationQueue which contains 2 NSOperations and is set to perform them one after another by setting setMaxConcurrentOperationCount
to 1.
One of the operations is a standard non-concurrent operation (just a main
method) which synchronously retrieves some data from the web (on the separate operation thread of course). The other operation is a concurrent operation as I need to use some code which has to run asynchronously.
The problem is that I have discovered that the concurrent operation only works if it is added to the queue first. If it comes after any non-concurrent operations, then strangely the start
method gets called fine, but after that method ends and I have setup my connection to callback a method, it never does. No further operations in the queue get executed after. It's as if it hangs after the start method returns, and no callbacks from any url connections get called!
If my concurrent operation is put first in the queue then it all works fine, the async callbacks work and the subsequent operation executes after it has completed. I don't understand at all!
You can see the test code for my concurrent NSOperation below, and I'm pretty sure it's solid.
Any help would be greatly appreciated!
Main Thread Observation:
I have just discovered that if the concurrent operation is first on the queue then the [start]
method gets called on the main thread. However, if it is not first on the queue (if it is after either a concurrent or non-concurrent) then the [start]
method is not called on the main thread. This seems important as it fits the pattern of my problem. What could be the reason for this?
Concurrent NSOperation Code:
@interface ConcurrentOperation : NSOperation {
BOOL executing;
BOOL finished;
}
- (void)beginOperation;
- (void)completeOperation;
@end
@implementation ConcurrentOperation
- (void)beginOperation {
@try {
// Test async request
NSURLRequest *r = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:@"http://www.google.com"]];
NSURLConnection *c = [[NSURLConnection alloc] initWithRequest:r delegate:self];
[r release];
} @catch(NSException * e) {
// Do not rethrow exceptions.
}
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(@"Finished loading... %@", connection);
[self completeOperation];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(@"Finished with error... %@", error);
[self completeOperation];
}
- (void)dealloc {
[super dealloc];
}
- (id)init {
if (self = [super init]) {
// Set Flags
executing = NO;
finished = NO;
}
return self;
}
- (void)start {
// Main thread? This seems to be an important point
NSLog(@"%@ on main thread", ([NSThread isMainThread] ? @"Is" : @"Not"));
// Check for cancellation
if ([self isCancelled]) {
[self completeOperation];
return;
}
// Executing
[self willChangeValueForKey:@"isExecuting"];
executing = YES;
[self didChangeValueForKey:@"isExecuting"];
// Begin
[self beginOperation];
}
// Complete Operation and Mark as Finished
- (void)completeOperation {
BOOL oldExecuting = executing;
BOOL oldFinished = finished;
if (oldExecuting) [self willChangeValueForKey:@"isExecuting"];
if (!oldFinished) [self willChangeValueForKey:@"isFinished"];
executing = NO;
finished = YES;
if (oldExecuting) [self didChangeValueForKey:@"isExecuting"];
if (!oldFinished) [self didChangeValueForKey:@"isFinished"];
}
// Operation State
- (BOOL)isConcurrent { return YES; }
- (BOOL)isExecuting { return executing; }
- (BOOL)isFinished { return finished; }
@end
Queuing Code
// Setup Queue
myQueue = [[NSOperationQueue alloc] init];
[myQueue setMaxConcurrentOperationCount:1];
// Non Concurrent Op
NonConcurrentOperation *op1 = [[NonConcurrentOperation alloc] init];
[myQueue addOperation:op1];
[op1 release];
// Concurrent Op
ConcurrentOperation *op2 = [[ConcurrentOperation alloc] init];
[myQueue addOperation:op2];
[op2 release];
I've discovered what the problem was!
These two priceless articles by Dave Dribin describe concurrent operations in great detail, as well as the problems that Snow Leopard & the iPhone SDK introduce when calling things asynchronously that require a run loop.
http://www.dribin.org/dave/blog/archives/2009/05/05/concurrent_operations/ http://www.dribin.org/dave/blog/archives/2009/09/13/snowy_concurrent_operations/
Thanks to Chris Suter too for pointing me in the right direction!
The crux of it is to ensure that the start
method us called on the main thread:
- (void)start {
if (![NSThread isMainThread]) { // Dave Dribin is a legend!
[self performSelectorOnMainThread:@selector(start) withObject:nil waitUntilDone:NO];
return;
}
[self willChangeValueForKey:@"isExecuting"];
_isExecuting = YES;
[self didChangeValueForKey:@"isExecuting"];
// Start asynchronous API
}
Your problem is most likely with NSURLConnection. NSURLConnection depends on a run loop running a certain mode (usually just the default ones).
There are a number of solutions to your problem:
Make sure that this operation only runs on the main thread. If you were doing this on OS X, you’d want to check that it does what you want in all run loop modes (e.g. modal and event tracking modes), but I don’t know what the deal is on the iPhone.
Create and manage your own thread. Not a nice solution.
Call -[NSURLConnection scheduleInRunLoop:forMode:] and pass in the main thread or another thread that you know about. If you do this, you probably want to call -[NSURLConnection unscheduleInRunLoop:forMode:] first otherwise you could be receiving the data in multiple threads (or at least that’s what the documentation seems to suggest).
Use something like +[NSData dataWithContentsOfURL:options:error:] and that will also simplify your operation since you can make it a non-concurrent operation instead.
Variant on #4: use +[NSURLConnection sendSynchronousRequest:returningResponse:error:].
If you can get away with it, do #4 or #5.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With