Why is NSOperationQueue.mainQueue.maxConcurrentOperationCount set to 1

956 Views Asked by At

The reason for this question is because of the reactions to this question.

I realized the understanding of the problem was not fully there as well as the reason for the question in the first place. So I am trying to boil down the reason for the other question to this one at it's core.

First a little preface, and some history, I know NSOperation(Queue) existed before GCD, and and they were implemented using threads before dispatch queues.

The next thing is that you need to understand is that by default, meaning no "waiting" methods being use on operations or operation queues (just a standard "addOperation:"), an NSOperation's main method is executed on the underlying queue of the NSOperationQueue asynchronously (e.g. dispatch_async()).

To conclude my preface, I'm questioning the purpose of setting NSOperationQueue.mainQueue.maxConcurrentOperationCount to 1 in this day and age, now that the underlyingQueue is actually the main GCD serial queue (e.g. the return of dispatch_get_main_queue()).

If NSOperationQueue.mainQueue already executes it's operation's main methods serially, why worry about maxConcurrentOperationCount at all?

To see the issue of it being set to 1, please see the example in the referenced question.

1

There are 1 best solutions below

0
On

It's set to 1 because there's no reason to set it to anything else, and it's probably slightly better to keep it set to 1 for at least three reasons I can think of.

Reason 1

Because NSOperationQueue.mainQueue's underlyingQueue is dispatch_get_main_queue(), which is serial, NSOperationQueue.mainQueue is effectively serial (it could never run more than a single block at a time, even if its maxConcurrentOperationCount were greater than 1).

We can check this by creating our own NSOperationQueue, putting a serial queue in its underlyingQueue target chain, and setting its maxConcurrentOperationCount to a large number.

Create a new project in Xcode using the macOS > Cocoa App template with language Objective-C. Replace the AppDelegate implementation with this:

@implementation AppDelegate {
    dispatch_queue_t concurrentQueue;
    dispatch_queue_t serialQueue;
    NSOperationQueue *operationQueue;
}

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    concurrentQueue = dispatch_queue_create("q", DISPATCH_QUEUE_CONCURRENT);
    serialQueue = dispatch_queue_create("q2", nil);
    operationQueue = [[NSOperationQueue alloc] init];

    // concurrent queue targeting serial queue
    //dispatch_set_target_queue(concurrentQueue, serialQueue);
    //operationQueue.underlyingQueue = concurrentQueue;

    // serial queue targeting concurrent queue
    dispatch_set_target_queue(serialQueue, concurrentQueue);
    operationQueue.underlyingQueue = serialQueue;

    operationQueue.maxConcurrentOperationCount = 100;

    for (int i = 0; i < 100; ++i) {
        NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"operation %d starting", i);
            sleep(3);
            NSLog(@"operation %d ending", i);
        }];
        [operationQueue addOperation:operation];
    }
}

@end

If you run this, you'll see that operation 1 doesn't start until operation 0 has ended, even though I set operationQueue.maxConcurrentOperationCount to 100. This happens because there is a serial queue in the target chain of operationQueue.underlyingQueue. Thus operationQueue is effectively serial, even though its maxConcurrentOperationCount is not 1.

You can play with the code to try changing the structure of the target chain. You'll find that if there is a serial queue anywhere in that chain, only one operation runs at a time.

But if you set operationQueue.underlyingQueue = concurrentQueue, and do not set concurrentQueue's target to serialQueue, then you'll see that 64 operations run simultaneously. For operationQueue to run operations concurrently, the entire target chain starting with its underlyingQueue must be concurrent.

Since the main queue is always serial, NSOperationQueue.mainQueue is effectively always serial.

In fact, if you set NSOperationQueue.mainQueue.maxConcurrentOperationCount to anything but 1, it has no effect. If you print NSOperationQueue.mainQueue.maxConcurrentOperationCount after trying to change it, you'll find that it's still 1. I think it would be even better if the attempt to change it raised an assertion. Silently ignoring attempts to change it is more likely to lead to confusion.

Reason 2

NSOperationQueue submits up to maxConcurrentOperationCount blocks to its underlyingQueue simultaneously. Since the mainQueue.underlyingQueue is serial, only one of those blocks can run at a time. Once those blocks are submitted, it may be too late to use the -[NSOperation cancel] message to cancel the corresponding operations. I'm not sure; this is an implementation detail that I haven't fully explored. Anyway, if it is too late, that is unfortunate as it may lead to a waste of time and battery power.

Reason 3

As with mentioned with reason 2, NSOperationQueue submits up to maxConcurrentOperationCount blocks to its underlyingQueue simultaneously. Since mainQueue.underlyingQueue is serial, only one of those blocks can execute at a time. The other blocks, and any other resources the dispatch_queue_t uses to track them, must sit around idly, waiting for their turns to run. This is a waste of resources. Not a big waste, but a waste nonetheless. If mainQueue.maxConcurrentOperationCount is set to 1, it will only submit a single block to its underlyingQueue at a time, thus preventing GCD from allocating resources uselessly.