Question

How can we implement dispatch_barrier_async's equivalent behavior using NSOperationQueue or any user-defined data-structure based on NSOperationQueue?

The requirement is, whenever a barrier operation is submitted it should wait until all non-barrier operations submitted earlier finish their execution and blocks other operations submitted after that.

  • Non-barrier operations should be able to perform concurrently.
  • Barrier operations should execute serially.

NB: Not using GCD,as it doesn't provide(or atleast difficult) much access over the operations, like cancelling single operation, etc.

Was it helpful?

Solution

This is more or less what jeffamaphone was saying, but I put up a gist that should, in rough outline, do what you ask.

I create a NSMutableArray of NSOperationQueues, which serves as a "queue of queues". Every time you add a BarrierOperation object, you tack a fresh suspended op queue on the end. That becomes the addingQueue, to which you add subsequent operations.

- (void)addOperation:(NSOperation *)op {
    @synchronized (self) {
        if ([op isKindOfClass:[BarrierOperation class]]) {
            [self addBarrierOperation:(id)op];
        } else {
            [[self addingQueue] addOperation:op];
        }
    }
}

// call only from @synchronized block in -addOperation:
- (void)addBarrierOperation:(BarrierOperation *)barrierOp {
    [[self addingQueue] setSuspended:YES];

    for (NSOperation *op in [[self addingQueue] operations]) {
        [barrierOp addDependency:op];
    }

    [[self addingQueue] addOperation:barrierOp];

    // if you are free to set barrierOp.completionBlock, you could skip popCallback and do that

    __block typeof(self) weakSelf = self;
    NSOperation *popCallback = [NSBlockOperation blockOperationWithBlock:^{
        [weakSelf popQueue];
    }];
    [popCallback addDependency:barrierOp];
    [[self addingQueue] addOperation:popCallback];
    [[self addingQueue] setSuspended:NO];

    NSOperationQueue *opQueue = [[NSOperationQueue alloc] init];
    [opQueue setSuspended:YES];

    [_queueOfQueues addObject:opQueue]; // fresh empty queue to add to
}

When one NSOperationQueue finishes, it gets popped and the next one starts running.

- (void)popQueue
{
    @synchronized (self) {
        NSAssert([_queueOfQueues count], @"should always be one to pop");
        [_queueOfQueues removeObjectAtIndex:0];

        if ([_queueOfQueues count]) {
            // first queue is always running, all others suspended
            [(NSOperationQueue *)_queueOfQueues[0] setSuspended:NO];
        }
    }
}

I might have missed something crucial. The devil's in the details.

This smells a bit like a homework assignment to me. If so, tell me what grade I get. :)


Addendum: Via abhilash1912's comment, a different but similar approach. That code is tested, so it already wins. But it is a bit stale (2 years or so as of today; some deprecated method usage). Moreover, I question whether inheriting from NSOperationQueue is the best path, though it has the virtue of retaining familiarity. Regardless, if you've read this far, it's probably worth looking over.

If you create or find the world's greatest BarrierQueue class, please let us know in the comments or otherwise, so it can be linked up.

OTHER TIPS

Create an NSOperation that is your barrier, then use:

- (void)addDependency:(NSOperation *)operation

To make that barrier operation dependent on all the ones you want to come before it.

I don't think it's possible to create an NSOperation object that's gives you the same sort of functionality, barriers have more to do with the way the queue operates.

The main difference between using a barrier and the dependency mechanism of NSOperations is, in the case of a barrier, the thread queue waits until all running concurrent operations have completed, and then it runs your barrier block, while making sure that any new blocks submitted and any blocks waiting do not run until the critical block has passed.

With an NSOperationQueue, it's impossible to set up the queue in such a way that it'll enforce a proper barrier: all NSOperations added to the queue before your critical NSOperation must be explicitly registered as a dependency with the critical job, and once the critical job has started, you must explicitly guard the NSOperationQueue to make sure no other clients push jobs onto it before the critical job has finished; you guard the queue by adding the critical job as a dependency for the subsequent operations.

(In the case where you know there's only one critical job at a time, this sounds sorta easy, but there will probably be n critical jobs waiting at any one time, which means keeping track of the order jobs are submitted, managing the relative dependency of critical jobs relative to their dependent jobs -- some critical jobs can wait for others, some must be executed in a particular order relative to others... yikes.)

It might be possible to get this level of functionality by setting up an NSOperationQueue with a concurrent job max of one, but that sorta defeats the purpose of doing this, I think. You might also be able to make this work by wrapping an NSOperationQueue in a facade object that protects NSOperations that are submitted "critically."

Just another way... don't hurt me.

Todo: save origin completion and self.maxConcurrentOperationCount = 1 sets queue to serial on adding. But should before execution.

#import "NSOperationQueue+BarrierOperation.h"

@implementation NSOperationQueue (BarrierOperation)

- (void)addOperationAsBarrier:(NSOperation *)op
{
    //TODO: needs to save origin completion
    //    if (op.completionBlock)
    //    {
    //        originBlock = op.completionBlock;
    //    }

    NSOperationQueue* qInternal = [NSOperationQueue new];
    NSInteger oldMaxConcurrentOperationCount = self.maxConcurrentOperationCount;

    op.completionBlock = ^{
        self.maxConcurrentOperationCount = oldMaxConcurrentOperationCount;
        NSLog(@"addOperationAsBarrier maxConcurrentOperationCount restored");
    };

    [self addOperationWithBlock:^{
        self.maxConcurrentOperationCount = 1;
        NSLog(@"addOperationAsBarrier maxConcurrentOperationCount = 1");
    }];

    [qInternal addOperationWithBlock:^{
        NSLog(@"waitUntilAllOperationsAreFinished...");
        [self waitUntilAllOperationsAreFinished];
    }];

    NSLog(@"added OperationAsBarrier");
    [self addOperation:op];
}

@end
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top