3

I'm having trouble understanding the way NSOperationQueue works.

Say I have:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        queue.maxConcurrentOperationCount=1;

        [queue addOperationWithBlock:^{
[someObject someSelector];
}];

        [queue addOperationWithBlock:^{
[someObject anotherSelector];
}];

The second block is being called even before the first block finishes - the opposite of what I want. I tried using – performSelectorOnMainThread:withObject:waitUntilDone: instead, but the second block is still being executed first - presumably because the block thread is not being completed on the main thread, and so it is not blocked with waitUntilDone. I added a break point inside my someSelector block, and it is reached after a break point inside the second block.

I don't quite get it. Help me!!

JoshDG
  • 3,871
  • 10
  • 51
  • 85
  • I still dont quite get it. What should I do to my code in order to guarantee FIFO? – JoshDG Nov 26 '13 at 18:21
  • I think you must set operation dependencies to guarantee operation ordering. – David Snabel-Caunt Nov 26 '13 at 18:24
  • Just using `addDependency` will do the job, unless either of these methods doing anything that is, itself, running asynchronously, in which case you will probably want to use a `NSOperation` subclass. – Rob Nov 26 '13 at 18:32
  • Ah yes thats my problem. Inside my someSelector method is a call to an NSObject that does a syncronous URL request, but I guess the call to that method is itself asynchronous. Specifically it looks like data = [customObject getDataWithURL:(NSString*)url] - but you can't call performSelectorWaitUntilDone on something with a return value. I was going to look into NSInvocation, would that be the right thing to do in this case? – JoshDG Nov 26 '13 at 18:48
  • I put that into a new question here: http://stackoverflow.com/questions/20225804/nsoperationqueue-with-syncronous-nsurlconnection – JoshDG Nov 26 '13 at 19:06

1 Answers1

4

If there are explicit dependencies between the operations, then use addDependency:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount=1;

NSOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    [someObject someSelector];
}];

NSOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
    [someObject anotherSelector];
}];

[operation2 addDependency:operation1];

[queue addOperation:operation1];
[queue addOperation:operation2];

If your operations are doing asynchronous activity, then you should define a custom operation, and only call completeOperation (which will post the isFinished message) when the asynchronous task is done).

//  SomeOperation.h

#import <Foundation/Foundation.h>

@interface SomeOperation : NSOperation

@end

and

//  SomeOperation.m

#import "SomeOperation.h"

@interface SomeOperation ()
@property (nonatomic, readwrite, getter = isFinished)  BOOL finished;
@property (nonatomic, readwrite, getter = isExecuting) BOOL executing;
@end

@implementation SomeOperation

@synthesize finished  = _finished;
@synthesize executing = _executing;

#pragma Configure basic operation

- (id)init
{
    self = [super init];
    if (self) {
        _finished  = NO;
        _executing = NO;
    }
    return self;
}

- (void)start
{
    if ([self isCancelled]) {
        self.finished = YES;
        return;
    }

    self.executing = YES;

    [self main];
}

- (void)completeOperation
{
    self.executing = NO;
    self.finished  = YES;
}

- (void)main
{
    // start some asynchronous operation

    // when it's done, call `completeOperation`
}

#pragma mark - Standard NSOperation methods

- (BOOL)isConcurrent
{
    return YES;
}

- (void)setExecuting:(BOOL)executing
{
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    [self didChangeValueForKey:@"isExecuting"];
}

- (void)setFinished:(BOOL)finished
{
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    [self didChangeValueForKey:@"isFinished"];
}

@end

Thus, with the following code, it won't start operation2 until the asynchronous task initiated in main in SomeOperation object, operation1, calls its completeOperation method.

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount=1;

NSOperation *operation1 = [[SomeOperation alloc] init];

NSOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
    [someObject anotherSelector];
}];

[operation2 addDependency:operation1];

[queue addOperation:operation1];
[queue addOperation:operation2];
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Doesnt solve my particular problem, but definitely answers the question that I asked. – JoshDG Nov 26 '13 at 18:49
  • @JoshDG I've updated my answer with sample code for wrapping your asynchronous task in a custom `NSOperation` which won't post `isFinished` until the asynchronous process is done. – Rob Nov 26 '13 at 19:13
  • Thanks so much I really appreciate all of that code! So, what ended up happening, as you suggested, was that I was putting the call to the NSURLConnection inside another NSOperationQueue, and I guess by doing that, it was put inside another thread? I ended up taking out the second operation queue and now it runs as expected. Did I reason that correctly? – JoshDG Nov 26 '13 at 21:06
  • @JoshDG Yep, that's makes complete sense: Make the block that you add to the queue, itself, synchronous (such as by eliminating the redundant operation queue), and that will fix it. The above technique for wrapping an asynchronous process in a custom `NSOperation` is a useful pattern when you cannot (or do not want to) make the submitted operation synchronous (e.g. usually people don't like using `sendSynchronousRequest` because you lose a lot of features). But it looks like you've got something that works for you. – Rob Nov 26 '13 at 22:15
  • I really appreciate your help! – JoshDG Nov 26 '13 at 22:40