8

i need to do a series of url calls (fetching WMS tiles). i want to use a LIFO stack so the newest url call is the most important. i want to display the tile on the screen now, not a tile that was on the screen 5 seconds ago after a pan.

i can create my own stack from a NSMutableArray, but i'm wondering if a NSOperationQueue can be used as a LIFO stack?

Padin215
  • 7,444
  • 13
  • 65
  • 103

6 Answers6

5

You can set the priority of operations in an operation queue using -[NSOperation setQueuePriority:]. You'll have to rejigger the priorities of existing operations each time you add an operation, but you can achieve something like what you're looking for. You'd essentially demote all of the old ones and give the newest one highest priority.

Ken Thomases
  • 88,520
  • 7
  • 116
  • 154
  • To quote from Apple's documentation: You should use priority values only as needed to classify the relative priority of non-dependent operations. Priority values should not be used to implement dependency management among different operation objects. Found it here: http://stackoverflow.com/questions/20297333/function-setqueuepriority-has-different-performance-on-ios-emulator-and-on-iphon. Trying to set order with setQueuePriority finished with random execution – Michal Gumny Jan 19 '14 at 12:18
  • @MichalGumny, the Apple doc quote validates my suggestion. We're trying to "classify the relative priority of non-dependent operations". We're not trying to implement dependency management. The queue, if not suspended, can start an operation as soon as it's added or whenever it is below its max. concurrency. If the other question you cited had suspended the queue it would have worked fine. The queue can start operations at any time and, obviously, changing the priority of operations won't change what's already happened. But the priority is respected when a new operation is picked to start. – Ken Thomases Jan 19 '14 at 17:19
  • Question was for LIFO order, Apple wrote "relative priority" which I think suggest that we can't be 100% sure that priority oder will be preserved. I even wrote little code to check it and order isn't exactly as supposed: http://pastebin.com/ZM8G9s2P. Ofc I can be wrong, please correct me if this is the case – Michal Gumny Jan 20 '14 at 10:26
  • Try using `-[NSOperationQueue setSuspended:]` instead of `-setMaxConcurrentOperationCount:` with `0`. I see no reason to believe that the latter will effectively suspend a queue. "Relative priority" doesn't mean it might not be obeyed. It simply means the priority values are not meaningful on any absolute scale but only in comparison to each other. When the queue is ready to start a new operation it will definitely pick the highest priority ready op. Due to the nature of concurrency, there can be race conditions, but other than that the priority will be honored. – Ken Thomases Jan 20 '14 at 15:14
  • From the [docs](https://developer.apple.com/library/mac/documentation/cocoa/reference/NSOperationQueue_class/Reference/Reference.html): "Operations within the queue … are … organized according to priority levels … and are executed accordingly." "For operations that are ready to execute, the operation queue always executes the one with the highest priority relative to the other ready operations." "An operation queue executes its queued operation objects based on their priority and readiness." – Ken Thomases Jan 20 '14 at 15:17
  • @MichalGumny: Oh, and you can't pass arbitrary values to `-[NSOperation setQueuePriority:]` like in your code. Or, rather, you can but the value will be adjusted to one of the predefined values. – Ken Thomases Jan 20 '14 at 15:19
3

Sadly I think NSOperationQueues are, as the name suggests, only usable as queues — not as stacks. To avoid having to do a whole bunch of manual marshalling of tasks, probably the easiest thing is to treat your queues as though they were immutable and mutate by copying. E.g.

- (NSOperationQueue *)addOperation:(NSOperation *)operation toHeadOfQueue:(NSOperationQueue *)queue
{
    // suspending a queue prevents it from issuing new operations; it doesn't
    // pause any already ongoing operations. So we do this to prevent a race
    // condition as we copy operations from the queue
    queue.suspended = YES;

    // create a new queue
    NSOperationQueue *mutatedQueue = [[NSOperationQueue alloc] init];

    // add the new operation at the head
    [mutatedQueue addOperation:operation];

    // copy in all the preexisting operations that haven't yet started
    for(NSOperation *operation in [queue operations])
    {
        if(!operation.isExecuting)
            [mutatedQueue addOperation:operation];
    }

    // the caller should now ensure the original queue is disposed of...
}

/* ... elsewhere ... */

NSOperationQueue *newQueue = [self addOperation:newOperation toHeadOfQueue:operationQueue];
[operationQueue release];
operationQueue = newQueue;

It seems at present that releasing a queue that is still working (as will happen to the old operation queue) doesn't cause it to cancel all operations, but that's not documented behaviour so probably not trustworthy. If you want to be really safe, key-value observe the operationCount property on the old queue and release it when it goes to zero.

Tommy
  • 99,986
  • 12
  • 185
  • 204
  • i belive this would work, i've decided to just create my own stack from a NSMutableArray. thanks for the input. – Padin215 Apr 10 '12 at 16:52
  • For the record, I would have avoided creating my own stack because you're then responsible for issuing operations and monitoring their progress, i.e. you end up reimplementing a lot of what `NSOperationQueue` already does. If you were using concurrent operations you'd pretty much definitely end up being less efficient. Obviously none of that is relevant to your specific needs and I'm sure you're doing the right thing, I just thought I'd say that for completeness. – Tommy Apr 10 '12 at 18:46
  • i'm creating a stack of URL's that i need to call for a WMS tile. i check if i have X tile, if its not in a cache, i put the URL on the stack. i have a timer that fires ever 2 seconds which will create the request and put it into a GDC queue to execute. – Padin215 Apr 10 '12 at 20:05
  • There is a good chance that this won’t work - the `operations` array isn’t guaranteed to contain the operations in the order they where added. And the order operations are added also isn’t necessarily the order in which they are executed, because of dependencies and different priorities. – Sven Aug 19 '12 at 20:13
1

I'm not sure if you're still looking a solution, but I've the same problem has been bugging me for a while, so I went ahead and implemented an operation stack here: https://github.com/cbrauchli/CBOperationStack. I've used it with a few hundred download operations and it has held up well.

cbrauchli
  • 1,555
  • 15
  • 25
1

Sadly, you cannot do that without running into some tricky issues, because:

Important You should always configure dependencies before running your operations or adding them to an operation queue. Dependencies added afterward may not prevent a given operation object from running. (From: Concurrency Programming Guide: Configuring Interoperation Dependencies)

Take a look at this related question: AFURLConnectionOperation 'start' method gets called before it is ready and never gets called again afterwards

Andrew
  • 7,630
  • 3
  • 42
  • 51
mutable.me
  • 351
  • 2
  • 9
0

Found a neat implementation of stack/LIFO features on top of NSOperationQueue. It can be used as a category that extends NSOperationQueue or an NSOperationQueue LIFO subclass.

https://github.com/nicklockwood/NSOperationStack

Ullullu
  • 487
  • 4
  • 10
  • Sadly, you cannot do that without running into some tricky issues: http://stackoverflow.com/questions/14821406/afurlconnectionoperation-start-method-gets-called-before-it-is-ready-and-never – mutable.me Nov 14 '15 at 20:11
0

The easiest way would be to separate your operations and data you will be processing, so you can add operations to NSOperationQueue as usual and then take data from a stack or any other data structure you need.

var tasks: [MyTask]
...

func startOperation() {
    myQueue.addOperation {
        guard let task = tasks.last else {
            return
        }

        tasks.removeLast()

        task.perform()
    }
}

Now, obviously, you might need to ensure that tasks collection can be used concurrently, but it's a much more common problem with lots of pre-made solutions than hacking your way around NSOperationQueue execution order.

Konstantin Oznobihin
  • 5,234
  • 24
  • 31