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
Cast Value of Type 'Uitableviewcell' to Custom Cell
How to Identify Ekevent Uniquely with Sync Across the Devices
Why I Couldn't Assign Fetched Values from Firestore to an Array in Swift
Xcode/Simulator: How to Run Older iOS Version
What Happens If My Distribution Certificate Expires
In-App Purchase in Swift with a Single Product
Get Latitude/Longitude from Address
How to Get All Enum Values as an Array
Improper Advertising Identifier [Idfa] Usage
Uiimagepickercontroller Camera View Rotating Strangely on iOS 8 (Pictures)
Google Analytics iOS Campaign Tracking and Url Builder
How to Get iOS Appstorereceipturl into Base 64 Encoded String
Setting Alpha on Uiview Sets the Alpha on Its Subviews Which Should Not Happen
iOS 9 Ats Ssl Error with Supporting Server
How to Set Multi Line Large Title in Navigation Bar? ( New Feature of iOS 11)
Cannot Assign Value of Type '() -> Void' to Type '(() -> Void)!'