Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Problems Queuing Concurrent & Non-Concurrent NSOperations

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];
like image 657
Michael Waterfall Avatar asked Oct 29 '09 09:10

Michael Waterfall


Video Answer


2 Answers

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

}
like image 66
Michael Waterfall Avatar answered Oct 19 '22 03:10

Michael Waterfall


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.

like image 36
Chris Suter Avatar answered Oct 19 '22 03:10

Chris Suter