views:

761

answers:

3

Hi there,

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];
A: 

I didn't notice, nor do I see any mention of addDependency:, which would seem like a prerequisite to getting operations to execute in the proper order.

In short, the second operation depends on the first.

johne
Oh yes I forgot to mention about the queue code. I haven't added any dependencies, but not for any reason really. All the operations are unrelated, but I just want them to run one after another. Would dependencies make any difference to the problem I'm having? I have set the setMaxConcurrentOperationCount to 1 and thought that would be enough. Thanks for your response!
Michael Waterfall
I've just added dependencies and it hasn't had any effect on my problem.
Michael Waterfall
+1  A: 

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:

  1. 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.

  2. Create and manage your own thread. Not a nice solution.

  3. 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).

  4. Use something like +[NSData dataWithContentsOfURL:options:error:] and that will also simplify your operation since you can make it a non-concurrent operation instead.

  5. Variant on #4: use +[NSURLConnection sendSynchronousRequest:returningResponse:error:].

If you can get away with it, do #4 or #5.

Chris Suter
Thanks for your response. The code I posted is just a simple example that I created for testing purposes. The actual program is using an API that is outside of my control, and is asynchronous in nature.However you did lead me to properly re-read 2 interesting articles which has resolved the issue! I'll post an answer to it so it's clear when others have the issue but thanks for taking the time and you're dead right about the run loops!
Michael Waterfall
+2  A: 

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

}
Michael Waterfall