How to Stop/Cancel/Suspend/Resume Tasks on Gcd Queue

How to stop a DispatchWorkItem in GCD?

GCD does not perform preemptive cancelations. So, to stop a work item that has already started, you have to test for cancelations yourself. In Swift, cancel the DispatchWorkItem. In Objective-C, call dispatch_block_cancel on the block you created with dispatch_block_create. You can then test to see if was canceled or not with isCancelled in Swift (known as dispatch_block_testcancel in Objective-C).

func testDispatchItems() {
let queue = DispatchQueue.global()

var item: DispatchWorkItem?

// create work item

item = DispatchWorkItem { [weak self] in
for i in 0 ... 10_000_000 {
if item?.isCancelled ?? true { break }
print(i)
self?.heavyWork()
}
item = nil // resolve strong reference cycle of the `DispatchWorkItem`
}

// start it

queue.async(execute: item!)

// after five seconds, stop it if it hasn't already

queue.asyncAfter(deadline: .now() + 5) {
item?.cancel()
item = nil
}
}

Or, in Objective-C:

- (void)testDispatchItem {
dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);

static dispatch_block_t block = nil; // either static or property

__weak typeof(self) weakSelf = self;

block = dispatch_block_create(0, ^{
for (long i = 0; i < 10000000; i++) {
if (dispatch_block_testcancel(block)) { break; }
NSLog(@"%ld", i);
[weakSelf heavyWork];
}

block = nil;
});

// start it

dispatch_async(queue, block);

// after five seconds, stop it if it hasn't already

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (block) { dispatch_block_cancel(block); }
});
}

How to pause a task scheduled by DispatchQueue]

If the task has already been launched, the easiest way to do this is to have an isCancelled variable that your task's code repeatedly checks inside its main loop. To cancel the task, you simply set the isCancelled variable to true. You may want to use a lock and/or semaphore to make the variable thread-safe. If you use OperationQueue instead of DispatchQueue, the Operation object contains an isCancelled operation which is ready-made for this purpose (and if you manage to set the isCancelled property before the operation has even started, OperationQueue will skip running it at all).

How to suspend a work item on the main queue

On that, is it possible to create a background queue that on execution, executes a work item on the main queue?

You are suggesting something like this:

var q = DispatchQueue(label: "myqueue")
func configAndStart(seconds:TimeInterval, handler:@escaping ()->Void) {
self.q.asyncAfter(deadline: .now() + seconds, execute: {
DispatchQueue.main.async(execute: handler())
})
}
func pause() {
self.q.suspend()
}
func resume() {
self.q.resume()
}

But my actual tests seem to show that that won't work as you desire; the countdown doesn't resume from where it was suspended.

One workaround I have considered is when the user performs the pause action, storing the time left until the work item was going to be executed and re-adding the work item to the queue with that time on the resume action. This seems like a poor quality approach and I feel there is a more appropriate method to this.

It isn't poor quality. There is no built-in mechanism for pausing a dispatch timer countdown, or for introspecting the timer, so if you want to do the whole thing on the main queue your only recourse is just what you said: maintain your own timer and the necessary state variables. Here is a rather silly mockup I hobbled together:

class PausableTimer {
var t : DispatchSourceTimer!
var d : Date!
var orig : TimeInterval = 0
var diff : TimeInterval = 0
var f : (()->Void)!
func configAndStart(seconds:TimeInterval, handler:@escaping ()->Void) {
orig = seconds
f = handler
t = DispatchSource.makeTimerSource()
t.schedule(deadline: DispatchTime.now()+orig, repeating: .never)
t.setEventHandler(handler: f)
d = Date()
t.resume()
}
func pause() {
t.cancel()
diff = Date().timeIntervalSince(d)
}
func resume() {
orig = orig-diff
t = DispatchSource.makeTimerSource()
t.schedule(deadline: DispatchTime.now()+orig, repeating: .never)
t.setEventHandler(handler: f)
t.resume()
}
}

That worked in my crude testing, and seems to be interruptible (pausable) as desired, but don't quote me; I didn't spend much time on it. The details are left as an exercise for the reader!

GCD: How to remove waiting tasks from serial queue?

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.



Related Topics



Leave a reply



Submit