When does an NSOperationQueue start its first operation?

Steven Fisher picture Steven Fisher · May 2, 2012 · Viewed 9.3k times · Source

I've created a test project in which I'm testing my assumptions about NSOperation and NSOperationQueue before using them in my main project.

My code is pretty simple, so I'm going to include all of it here. This is using a command line Foundation project with ARC turned on.

Operation.h

#import <Foundation/Foundation.h>

@interface Operation : NSOperation

@property (readwrite, strong) NSString *label;

- (id)initWithLabel: (NSString *)label;

@end

Operation.m

#import "Operation.h"

@implementation Operation

- (void)main
{
    NSLog( @"Operation %@", _label);
}

- (id)initWithLabel: (NSString *)label
{
    if (( self = [super init] )) {
        _label = label;
    }
    return self;
}

@end

Main.m

#import <Foundation/Foundation.h>
#import "Operation.h"

int main(int argc, const char * argv[])
{

    @autoreleasepool {

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

        id create = [[Operation alloc] initWithLabel: @"create"];
        id update1 = [[Operation alloc] initWithLabel: @"update1"];
        id update2 = [[Operation alloc] initWithLabel: @"update2"];

        [update1 addDependency: create];
        [update2 addDependency: create];

        [queue addOperation: update1];
        [queue addOperation: create];
        [queue addOperation: update2];

        [queue waitUntilAllOperationsAreFinished];

    }
    return 0;
}

My output looks like this:

2012-05-02 11:37:08.573 QueueTest[1574:1b03] Operation create
2012-05-02 11:37:08.584 QueueTest[1574:1903] Operation update2
2012-05-02 11:37:08.586 QueueTest[1574:1b03] Operation update1

Having written this and experimented with a few combinations, I found that when I reordered the queue setup like this:

    [queue addOperation: update1];
    [queue addOperation: create];
    [queue addOperation: update2];

    [update1 addDependency: create];
    [update2 addDependency: create];

    [queue waitUntilAllOperationsAreFinished];

I got the same output:

2012-05-02 11:38:23.965 QueueTest[1591:1b03] Operation create
2012-05-02 11:38:23.975 QueueTest[1591:1b03] Operation update1
2012-05-02 11:38:23.978 QueueTest[1591:1903] Operation update2

I should note that I have found in some runs that update2 is executed before update1, but that behaviour isn't surprising. Why should NSOperationQueue be deterministic when I haven't asked it to be?

What I do find surprising is that somehow create is always executed before update1 and update2 even if everything is added to the queue before dependancies are added.

Obviously, this is a silly thing to do, but it's led me to wonder: Is the delay between when I add an operation to the queue and when it's executed documented, or in any way predictable? Exactly when does NSOperationQueue start processing added operations?

Really, most importantly, exactly what is NSOperationQueue waiting for and when will that wait bite me in some way I can't defend against?

Answer

Peter Hosey picture Peter Hosey · May 2, 2012

Obviously, this is a silly thing to do, but it's led me to wonder: Is the delay between when I add an operation to the queue and when it's executed documented, or in any way predictable? Exactly when does NSOperationQueue start processing added operations?

After:

  • you add the operation to the queue, and
  • all of the operation's dependencies have finished, and
  • the queue has nothing better (in terms of priority) to do.

For an operation with no unsatisfied dependencies, this can mean immediately.

Further experimentation seems to show that NSOperationQueue starts running operations as soon as the current thread yields control to it, via [queue waitUntilAllOperationsAreFinished], [NSThread sleepForTimeInterval: 0.000001], or an interrupted thread.

This assumption is bogus, for two reasons:

  1. Preemptive multitasking, which is done on every version of OS X ever (as well as every version of iOS), means that the scheduler can interrupt your thread at any time. You don't need to yield control to lose control.
  2. Multiple cores, which every current OS X and iOS device have, mean that the operation could start literally before addOperation: even returns (though this extreme is unlikely simply because of overhead). More importantly, the other operations may be able to run while the first one does.

Both of these points, but especially the latter, also mean that the question of “order” is pretty much reduced to nonsense. Operations that you arrange in a dependency tree will be started in order down that tree, but otherwise there is no sequence at all; you should assume that operations with no dependency between them will run at the same time, not one before another.

Once an operation has been started, adding dependencies to it has no effect. Since that can happen immediately upon adding it to the queue, you must add dependencies to the operation before you add the operation to a queue if you want the dependencies to reliably have effect.