5

I have a number of tasks I need to execute serially but the task includes next block in a completion block.

What is a good technique for doing these tasks one at a time, starting the next task after the current one completes its completion block?

Is there a technique other than a NSOperation subclass with a serial NSOperationQueue?

some_id
  • 29,466
  • 62
  • 182
  • 304
  • Are they all NSOperations? – jer-k Mar 13 '13 at 22:40
  • Do you not want to subclass `NSOperation`? Or do you have some objection to serial `NSOperationQueue`? – Rob Mar 13 '13 at 22:55
  • They are added as blocks using addOperationWithBlock. The async methods are not waited on so the concurrency of 1 on the queue does nothing. – some_id Mar 13 '13 at 22:55
  • @Helium3 This is an "older" question, but this code sample on Gist may help in your specific problem: [transform_each.m](https://gist.github.com/couchdeveloper/6155227). There is also a library modeled after the "Promise" definition in other languages (which I'm the author) where it becomes easy to manage complex asynchronous patterns. It's very light-weight and the API is extremely simple. You may take a look here: [RXPromise](https://github.com/couchdeveloper/RXPromise). – CouchDeveloper Aug 07 '13 at 09:46

3 Answers3

7

Standard solutions:

  1. NSOperationQueue with maxConcurrentOperationCount of 1. You say you don't want to do that, but you don't say why. Serial queues are the most logical solution.

    For example:

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    queue.maxConcurrentOperationCount = 1;
    
    [queue addOperationWithBlock:^{
        NSLog(@"Starting #1");
        sleep(3);
        NSLog(@"Finishing #1");
    }];
    
    [queue addOperationWithBlock:^{
        NSLog(@"Starting #2");
        sleep(3);
        NSLog(@"Finishing #2");
    }];
    
    [queue addOperationWithBlock:^{
        NSLog(@"Starting #3");
        sleep(3);
        NSLog(@"Finishing #3");
    }];
    
  2. If you don't want a serial NSOperationQueue, you can use a standard concurrent queue, but just make each operation dependent upon the prior one. You'll achieve the serial behavior you're looking for without using a serial queue.

    For example:

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    NSOperation *operation;
    NSOperation *previousOperation;
    
    operation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"Starting #1");
        sleep(3);
        NSLog(@"Finishing #1");
    }];
    [queue addOperation:operation];
    
    previousOperation = operation;
    operation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"Starting #2");
        sleep(3);
        NSLog(@"Finishing #2");
    }];
    [operation addDependency:previousOperation];
    [queue addOperation:operation];
    
    previousOperation = operation;
    operation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"Starting #3");
        sleep(3);
        NSLog(@"Finishing #3");
    }];
    [operation addDependency:previousOperation];
    [queue addOperation:operation];
    
  3. You could also create a GCD serial queue with dispatch_queue_create. It achieves the same thing as the first option, except using GCD instead of NSOperationQueue.

    For example:

    dispatch_queue_t queue = dispatch_queue_create("com.ConnerDouglass.operationtest", 0);
    
    dispatch_async(queue, ^{
        NSLog(@"Starting #1");
        sleep(3);
        NSLog(@"Finishing #1");
    });
    
    dispatch_async(queue, ^{
        NSLog(@"Starting #2");
        sleep(3);
        NSLog(@"Finishing #2");
    });
    
    dispatch_async(queue, ^{
        NSLog(@"Starting #3");
        sleep(3);
        NSLog(@"Finishing #3");
    });
    
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • 1
    Thanks. The thing is I add the method call using addOperationWithBlock, but as they are async methods I get an error "The operation couldn’t be completed. Too many open files" even though the concurrency is set to 1, the blocks are being marked as finished as the async calls are not being waited on. – some_id Mar 13 '13 at 22:55
  • @Helium3 Can you update your question with the nature of the code that you're dispatching to the background? When queueing operations that are, themselves, asynchronous, there are different approaches, but it would be helpful if you could be a little more specific. In my file downloader class, I subclass NSOperation and don't set `isFinished` until the download is done, but I don't want to continue guessing about what you're trying to do. – Rob Mar 13 '13 at 23:00
  • @Helium3 In addition to subclassing `NSOperation` and manually controlling when you set `isFinished`, you can also create two operations for every file, one which the start of the async operation, another which is a completion handler for the async operation. And you can make the next operation dependent upon the completion operation of the previous one. See http://stackoverflow.com/questions/14195706/multiple-locations-in-google-map-mkmapitem/14198584#14198584 for an example. – Rob Mar 13 '13 at 23:06
  • Thanks, will take a look. Does a block operation get destroyed when isFinished is set or is the queue more intellegent and e.g. know when a blocks execution finishes and the block is deallocated (async method completes), does the queue then process the next block? If so, setting depenedency on current block to previous block for each block could solve it – some_id Mar 13 '13 at 23:16
  • @Helium3 `NSBlockOperation` only knows when the added block returns; you can't asynchronously set "isFinished" in that scenario. Hence my two very different solutions: Either subclass a standard `NSOperation`, setting `isFinished` when the async op has its completion block called (and you may have to add a `NSRunLoop` for this sort of stuff to work) or play that clumsy trick in my previously mentioned answer with two ops (block or otherwise) per task, one for initiating an async task and one for its completion and make next op dependent on completion op of prior one. – Rob Mar 14 '13 at 03:26
  • 1
    Thanks for the suggestions. Due to the complexity of the block chain I decided to post a notification to a simple queue manager running a concurrent number of the tasks. – some_id Mar 14 '13 at 21:24
  • 1
    @Helium3 I'm sympathetic to this position. In my downloader class, I did the same thing (though I used a delegate-protocol pattern). I had reservations about writing my own queue manager, so I went back and employed Cocoa's `NSOperationQueue` and I've been surprised how complicated my operations became in order to support their invocation of other asynchronous methods. Looking at [MVCNetworking](https://developer.apple.com/library/ios/#samplecode/MVCNetworking/Introduction/Intro.html#//apple_ref/doc/uid/DTS40010443), I was surprised that I had to create additional `NSThread` to support it. – Rob Mar 14 '13 at 21:33
  • Yes exactly. The operations get extremely complicated for a block based system, although breaking the blocks into individual operations and grouping them with dependencies is a good design. Notification vs a massive refactor with possible regressions? The notification system is actually a clean solution imo. – some_id Mar 14 '13 at 21:43
1

I think this is an interesting solution: https://github.com/berzniz/Sequencer

Blake Schwendiman
  • 442
  • 1
  • 4
  • 9
0

What about something like this:

-(void)start
{
    // Start the async chain
    [self performSelectorInBackground:@selector(action1) withObject:nil];
}
-(void)notifyDone:(NSNumber *)taskID
{
    NSLog(@"Done with task #%i", taskID.intValue);
}
-(void)action1
{
    // Do some fancy async stuff here
    // Now, we are done. Notify the main thread that task 1 is complete.
    [self performSelectorOnMainThread:@selector(nofityDone:) withObject:[NSNumber numberWithInt:1] waitUntilDone:YES];
    // Move onto the next task once the main thread is done handling the notification
    [self action2];
}
-(void)action2
{
    // Do the same sort of thing as "action1" did, then call he next method
}