GCD: How to remove waiting tasks from serial queue?
Asked Answered
S

4

13

First I create a serial queue like this

static dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);

then, at some unknown point in time a task gets added to the queue like this

dispatch_async(queue, ^{
    // do something, which takes some time
});

If the first task hasn't finished yet, the new task will wait until the first completes (that's of course what a serial queue is for).

But if I add 5 new tasks to the queue, while the original first one is still running, I don't want to execute new task no.1, then no.2, then no.3 and so on, but want to get rid of tasks 1 to 4 and directly start executing task no.5 after the original first task has finished.

In other words, I want to pop any waiting task (not the one that is currently running) off the queue, if I add a new one.

Is there a build in mechanism for this or do I have to implement this myself? And for the latter, how would I identify single tasks inside a queue and remove them?

Selfmade answered 7/9, 2012 at 11:24 Comment(0)
I
20

Once a block has been submitted to a GCD dispatch queue, it will run. There is no way to cancel it. You can, as you know, implement your own mechanism to "abort" the block execution early.

An easier way to do this would be to use NSOperationQueue, as it already provides an implementation for canceling pending operations (i.e., those not yet running), and you can easily enqueue a block with the new-ish addOperationWithBlock method.

Though NSOperationQueue is implemented using GCD, I find GCD much easier to use in most cases. However, in this case, I would seriously consider using NSOperationQueue because it already handles canceling pending operations.

Illness answered 7/9, 2012 at 14:39 Comment(0)
S
5

With Davids answer getting me on track I succeeded in doing this like so

taskCounter++;
dispatch_async(queue, ^{
    if (taskCounter > 1) {
        taskCounter--;
        NSLog(@"%@", @"skip");
        return;
    }
    NSLog(@"%@", @"start");
    // do stuff
    sleep(3);
    taskCounter--;
    NSLog(@"%@", @"done");
});

taskCounter has to be either an ivar or a property (initialize it with 0). In that case it doesn't even need the __block attribute.

Selfmade answered 7/9, 2012 at 12:26 Comment(3)
I keep forgetting that when you have an ivar in a block (not using 'self.'), that the compiler actually generates code 'self->ivar', and thus it retains self and as you say no '__block' is required (this was discussed on the ObjectiveC Apple listserv, I had raised the question of why it was working!)Lincoln
I'm sure you know, but that implementation is not thread-safe. If multiple threads call this code, you will trash your taskCounter.Illness
Yep, that's true. Doesn't need to be thread-safe in the scenario I'm facing, but you're definitely right. Also, thanks for the anser about NSOperationQueues. I will consider using them instead, but there are other things to be taken care of first ;)Selfmade
L
4

The way you handle this is to use an ivar that indicates to the queued blocks they should just return:

^{
  if(!canceled) {
    ... do work
  }
}

You don't need to use a simple boolean either - you can make this more complex - but the general idea is to use one or more ivars that the block queries before doing anything.

I use this technique (but did not invent it) with great success.

Lincoln answered 7/9, 2012 at 11:28 Comment(0)
A
0

If instead of adding a closure in you add a DispatchWorkItem, you can cancel it as long as it hasn't started executing yet.

In the following code, backgroundWorkItem will never run, because it is cancelled before it starts executing.

let backgroundWorkItem = DispatchWorkItem {
    print("Background work item executed")
}

DispatchQueue.main.async(execute: backgroundWorkItem)

backgroundWorkItem.cancel()

Audiometer answered 4/11, 2022 at 23:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.