2
votes

I have an application in which a long running process (> 1 min) is placed onto an NSOperationQueue (Queue A). The UI is fully-responsive while the Queue A operation runs, exactly as expected.

However, I have a different kind of operation the user can perform which runs on a completely separate NSOperationQueue (Queue B).

When a UI event triggers the placement of an operation on Queue B, it must wait until after the currently-executing operation on Queue A finishes. This occurs on an iPod Touch (MC544LL).

What I expected to see instead was that any operation placed onto Queue B would more or less begin immediately executing in parallel with the operation on Queue A. This is the behavior I see on the Simulator.

My question is two parts:

  • Is the behavior I'm seeing on my device to be expected based on available documentation?
  • Using NSOperation/NSOperationQueue, how do I pre-empt the currently running operation on Queue A with a new operation placed on Queue B?

Note: I can get exactly the behavior I'm after by using GCD queues for Queues A/B, so I know my device is capable of supporting what I'm trying to do. However, I really, really want to use NSOperationQueue because both operations need to be cancelable.

I have a simple test application:

enter image description here

The ViewController is:

//
//  ViewController.m
//  QueueTest
//

#import "ViewController.h"

@interface ViewController ()

@property (strong, nonatomic) NSOperationQueue *slowQueue;
@property (strong, nonatomic) NSOperationQueue *fastQueue;

@end

@implementation ViewController

-(id)initWithCoder:(NSCoder *)aDecoder
{
    if (self = [super initWithCoder:aDecoder]) {
        self.slowQueue = [[NSOperationQueue alloc] init];
        self.fastQueue = [[NSOperationQueue alloc] init];
    }

    return self;
}

-(void)viewDidLoad
{
    NSLog(@"View loaded on thread %@", [NSThread currentThread]);
}

// Responds to "Slow Op Start" button
- (IBAction)slowOpStartPressed:(id)sender {
    NSBlockOperation *operation = [[NSBlockOperation alloc] init];

    [operation addExecutionBlock:^{
        [self workHard:600];
    }];

    [self.slowQueue addOperation:operation];
}

// Responds to "Fast Op Start" button
- (IBAction)fastOpStart:(id)sender {    
    NSBlockOperation *operation = [[NSBlockOperation alloc] init];

    [operation addExecutionBlock:^{
        NSLog(@"Fast operation on thread %@", [NSThread currentThread]);
    }];

    [self.fastQueue addOperation:operation];
}

-(void)workHard:(NSUInteger)iterations
{
    NSLog(@"SlowOperation start on thread %@", [NSThread currentThread]);

    NSDecimalNumber *result = [[NSDecimalNumber alloc] initWithString:@"0"];

    for (NSUInteger i = 0; i < iterations; i++) {        
        NSDecimalNumber *outer = [[NSDecimalNumber alloc] initWithUnsignedInteger:i];

        for (NSUInteger j = 0; j < iterations; j++) {
            NSDecimalNumber *inner = [[NSDecimalNumber alloc] initWithUnsignedInteger:j];
            NSDecimalNumber *product = [outer decimalNumberByMultiplyingBy:inner];

            result = [result decimalNumberByAdding:product];
        }

        result = [result decimalNumberByAdding:outer];
    }

    NSLog(@"SlowOperation end");
}

@end

The output I see after first pressing the "Slow Op Start" button followed ~1 second later by pressing the "Fast Op Start" button is:

2012-11-28 07:41:13.051 QueueTest[12558:907] View loaded on thread <NSThread: 0x1d51ec30>{name = (null), num = 1}
2012-11-28 07:41:14.745 QueueTest[12558:1703] SlowOperation start on thread <NSThread: 0x1d55e5f0>{name = (null), num = 3}
2012-11-28 07:41:25.127 QueueTest[12558:1703] SlowOperation end
2012-11-28 07:41:25.913 QueueTest[12558:3907] Fast operation on thread <NSThread: 0x1e36d4c0>{name = (null), num = 4}

As you can see, the second operation does not begin executing until after the first operation finishes, despite the fact that these are two separate (and presumably independent) NSOperationQueues.

I have read the Apple Concurrency Guide, but find nothing describing this situation. I've also read two SO questions on related topics (link, link), but neither seems to get to the heart of the problem I'm seeing (pre-emption).

Other things I've tried:

  • setting the queuePriority on each NSOperation
  • setting the queuePriority on each NSOperation while placing both types of operations onto the same queue
  • placing both operations onto the same queue

This question has undergone multiple edits, which may make certain comments/answers difficult to understand.

1
I see you cured the symptom while I was typing my answer :-) I suspect making the slow operation queue serial will do what you need if you require the operation management facilities of NSOperationQueue and can't use GCD.Simon Lawrence
@SimonLawrence, yes - our updates crossed paths :). Unfortunately, making the queue serial with [slowQueue setMaxConcurrentOperationCount:1] does not help. Is there another way?Rich Apodaca
In the code in your question, you're adding both operations to the same queue. Typo, or the cause of all your problems?jrturton
Typo left over from a variation I tried. Same behavior whether adding both ops to same or different queues.Rich Apodaca
Hi Rich, did you try my suggestion with an NSOperation subclass that runs the blocks without using GCD (see PseudoBlockOperation below)? That should do what you need for the long-running operations and is probably more appropriate than NSBlockOperation for these.Simon Lawrence

1 Answers

1
votes

I suspect the problem you are having is that both operation queues are executing their blocks on the underlying default priority dispatch queue. Consequently, if several slow operations are enqueued before the fast operations then perhaps you will see this behaviour.

Why not either set the NSOperationQueue instance for the slow operations so that it only executes one operation at any given time (i.e. set maxConcurrentOperationCount to one for this queue), or if your operations are all blocks then why not use GCD queues directly? e.g.

static dispatch_queue_t slowOpQueue = NULL;
static dispatch_queue_t fastOpQueue = NULL;

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    slowOpQueue = dispatch_queue_create("Slow Ops Queue", NULL);
    fastOpQueue = dispatch_queue_create("Fast Ops Queue", DISPATCH_QUEUE_CONCURRENT);
});

for (NSUInteger slowOpIndex = 0; slowOpIndex < 5; slowOpIndex++) {
    dispatch_async(slowOpQueue, ^(void) {
        NSLog(@"* Starting slow op %d.", slowOpIndex);
        for (NSUInteger delayLoop = 0; delayLoop < 1000; delayLoop++) {
            putchar('.');
        }

        NSLog(@"* Ending slow op %d.", slowOpIndex);
    });
}

for (NSUInteger fastBlockIndex = 0; fastBlockIndex < 10; fastBlockIndex++) {
    dispatch_async(fastOpQueue, ^(void) {
        NSLog(@"Starting fast op %d.", fastBlockIndex);
        NSLog(@"Ending fast op %d.", fastBlockIndex);
    });
}

As far as using the NSOperationQueue as per your comments about needing the operation cancellation facilities etc. can you try:

- (void)loadSlowQueue
{
    [self.slowQueue setMaxConcurrentOperationCount:1];
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"begin slow block 1");

        [self workHard:500];

        NSLog(@"end slow block 1");
    }];

    NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"begin slow block 2");

        [self workHard:500];

        NSLog(@"end slow block 2");
    }];

    [self.slowQueue addOperation:operation];
    [self.slowQueue addOperation:operation2];
}

As I think the two blocks you add to the operation on the slow queue are being executed in parallel on the default queue and preventing your fast operations from being scheduled.

Edit:

If you're still finding the default GCD queue is choking, why not create an NSOperation subclass that executes blocks without using GCD at all for your slow operations, this will still give you the declarative convenience of not creating a separate subclass for each operation but use the threading model of a regular NSOperation. e.g.

#import <Foundation/Foundation.h>

typedef void (^BlockOperation)(NSOperation *containingOperation);

@interface PseudoBlockOperation : NSOperation

- (id)initWithBlock:(BlockOperation)block;
- (void)addBlock:(BlockOperation)block;

@end

And then for the implementation:

#import "PseudoBlockOperation.h"

@interface PseudoBlockOperation()

@property (nonatomic, strong) NSMutableArray *blocks;

@end

@implementation PseudoBlockOperation

@synthesize blocks;

- (id)init
{
    self = [super init];

    if (self) {
        blocks = [[NSMutableArray alloc] initWithCapacity:1];
    }

    return self;
}

- (id)initWithBlock:(BlockOperation)block
{
    self = [self init];

    if (self) {
        [blocks addObject:[block copy]];
    }

    return self;
}

- (void)main
{
    @autoreleasepool {
        for (BlockOperation block in blocks) {
            block(self);
        }
    }
}

- (void)addBlock:(BlockOperation)block
{
    [blocks addObject:[block copy]];
}

@end

Then in your code you can do something like:

PseudoBlockOperation *operation = [[PseudoBlockOperation alloc] init];
[operation addBlock:^(NSOperation *operation) {
    if (!operation.isCancelled) {
        NSLog(@"begin slow block 1");

        [self workHard:500];

        NSLog(@"end slow block 1");
    }
}];

[operation addBlock:^(NSOperation *operation) {
    if (!operation.isCancelled) {
        NSLog(@"begin slow block 2");

        [self workHard:500];

        NSLog(@"end slow block 2");
    }
}];

[self.slowQueue addOperation:operation];

Note that in this example any blocks that are added to the same operation will be executed sequentially rather than concurrently, to execute concurrently create one operation per block. This has the advantage over NSBlockOperation in that you can pass parameters into the block by changing the definition of BlockOperation - here I passed the containing operation, but you could pass whatever other context is required.

Hope that helps.