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); }
});
}
Swift GCD Work Item Dispatch queue cancelling
You need to declare DispatchWorkItem variable at the top of class. In your code work1 variable is becoming inaccessible as soon as compiler is out of function. Every time you change your switch a new DispatchWorkItem variable initialize. Please go through below example for correct use of DispatchWorkItem with stop/resume functionality
@IBOutlet weak var button: UIButton!
@IBOutlet weak var label: UILabel!
var isStart = false
var work: DispatchWorkItem!
private func getCurrentTime() -> DispatchWorkItem {
work = DispatchWorkItem { [weak self] in
while true {
if (self?.work.isCancelled)!{break}
let date = Date()
let component = Calendar.current.dateComponents([.second], from: date)
DispatchQueue.main.async {
self?.label.text = "\(component.second!)"
}
}
}
return work
}
@IBAction func btnPressed() {
isStart = !isStart
button.setTitle(isStart ? "Stop" : "Start", for: .normal)
let workItem = getCurrentTime()
let globalQueue = DispatchQueue.global(qos: .background)
if isStart {
globalQueue.async(execute: workItem)
} else {
workItem.cancel()
}
}
This example will display current time seconds value in label. I have used while true, just to show an example.
How to avoid data race with GCD DispatchWorkItem.notify?
EDIT (2019-01-07): As mentioned by @Rob in a comment on the question, this can't be reproduced anymore with recent versions of Xcode/Foundation (I don't have Xcode installed anymore, I won't guess a version number). There is no workaround required.
Well looks like I found out. Using a DispatchGroup.notify
to get notified when the group's dispatched items have completed, instead of DispatchWorkItem.notify
, avoids the data race. Here's the same-ish snippet without the data race:
private func incrementAsync() {
let queue = DispatchQueue.global(qos: .background)
let item = DispatchWorkItem { [weak self] in
guard let strongSelf = self else { return }
strongSelf.x += 1
}
let group = DispatchGroup()
group.notify(queue: .main) { [weak self] in
guard let strongSelf = self else { return }
print("> \(strongSelf.x)")
}
queue.async(group: group, execute: item)
}
So DispatchGroup
introduces a happens-before relationship and notify
is safely called after the threads (in this case, a single async work item) finished execution, while DispatchWorkItem.notify
doesn't offer this guarantee.
Swift iOS -DispatchWorkItem is still running even though it's getting Cancelled and Set to Nil
This cancel routine is not doing what I suspect you think it is. When you cancel a DispatchWorkItem
, it performs no preemptive cancellation. It certainly has no bearing on the updateChildValues
call. All it does is perform a thread-safe setting of the isCancelled
property, which if you were manually iterating through a loop, you could periodically check and exit prematurely if you see that the task was canceled.
As a result, the checking of isCancelled
at the start of the task isn't terribly useful pattern, because if the task has not yet been created, there is nothing to cancel. Or if the task has been created and added to a queue, and canceled before the queue had a chance to start, it will obviously just be canceled but never started, you'll never get to your isCancelled
test. And if the task has started, it's likely gotten past the isCancelled
test before cancel
was called.
Bottom line, attempts to time the cancel
request so that they are received precisely after the task has started but before it has gotten to the isCancelled
test is going to be an exercise in futility. You have a race that will be almost impossible to time perfectly. Besides, even if you did happen to time this perfectly, this merely demonstrates how ineffective this whole process is (only 1 in a million cancel
requests will do what you intended).
Generally, if you had asynchronous task that you wanted to cancel, you'd wrap it in an asynchronous custom Operation
subclass, and implement a cancel
method that stops the underlying task. Operation queues simply offer more graceful patterns for canceling asynchronous tasks than dispatch queues do. But all of this presumes that the underlying asynchronous task offers a mechanism for canceling it and I don't know if Firebase even offers a meaningful mechanism to do that. I certainly haven't seen it contemplated in any of their examples. So all of this may be moot.
I'd suggest you step away from the specific code pattern in your question and describe what you are trying to accomplish. Let's not dwell on your particular attempted solution to your broader problem, but rather let's understand what the broader goal is, and then we can talk about how to tackle that.
As an aside, there are other technical issues in your example.
Specifically, I'm assuming you're running this on the main queue. So task.perform()
runs it on the current queue immediately. But your DispatchQueue.main.asyncAfter(...)
can only be run when whatever is running on the main queue is done. So, even though you specified a delay of 0.0000000001 seconds, it actually won't run until the main queue is available (namely, after your perform
is done running on the main queue and you're well past the isCancelled
test).
If you want to test this race between running the task and canceling the task, you need to perform the cancel on a different thread. For example, you could try:
weak var task: DispatchWorkItem?
let item = DispatchWorkItem {
if (task?.isCancelled ?? true) {
print("canceled")
} else {
print("not canceled in time")
}
}
DispatchQueue.global().asyncAfter(deadline: .now() + 0.00001) {
task?.cancel()
}
task = item
DispatchQueue.main.async {
item.perform()
}
Now you can play with various delays and see the different behavior between a delay of 0.1 seconds and one of 0.0000000001 seconds. And you'll want to make sure the app has reached quiescence before you try this test (e.g. do it on a button press event, not in viewDidLoad
).
But again, this will merely illustrate the futility of the whole exercise. You're going to have a really hard time catching the task between the time it started and before it checked the isCancelled
property. If you really want to manifest the cancel logic in some repeatable manner, we're going to have to artificially make this happen:
weak var task: DispatchWorkItem?
let queue = DispatchQueue(label: "com.domain.app.queue") // create a queue for our test, as we never want to block the main thread
let semaphore = DispatchSemaphore(value: 0)
let item = DispatchWorkItem {
// You'd never do this in a real app, but let's introduce a delay
// long enough to catch the `cancel` between the time the task started.
//
// You could sleep for some interval, or we can introduce a semphore
// to have it not proceed until we send a signal.
print("starting")
semaphore.wait() // wait for a signal before proceeding
// now let's test if it is cancelled or not
if (task?.isCancelled ?? true) {
print("canceled")
} else {
print("not canceled in time")
}
}
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
task?.cancel()
semaphore.signal()
}
task = item
queue.async {
item.perform()
}
Now, you'd never do this, but it just illustrates that isCancelled
does work.
Frankly, you'd never use isCancelled
like this. You would generally use the isCancelled
process if doing some long process where you can periodically check the isCancelled
status and exit if it is true. But that's not the case in your situation.
The conclusion of all of this is that checking isCancelled
at the start of a task is unlikely to ever achieve what you had hoped for.
How to get cancellation state for multiple DispatchWorkItems
The trick is how to have a dispatched task check if it has been canceled. I'd actually suggest consider OperationQueue
approach, rather than using dispatch queues directly.
There are at least two approaches:
Most elegant, IMHO, is to just subclass
Operation
, passing whatever you want to it in theinit
method, and performing the work in themain
method:class SearchOperation: Operation {
private var queryString: String
init(queryString: String) {
self.queryString = queryString
super.init()
}
override func main() {
// do something synchronous, periodically checking `isCancelled`
// e.g., for illustrative purposes
print("starting \(queryString)")
for i in 0 ... 10 {
if isCancelled { print("canceled \(queryString)"); return }
print(" \(queryString): \(i)")
heavyWork()
}
print("finished \(queryString)")
}
func heavyWork() {
Thread.sleep(forTimeInterval: 0.5)
}
}Because that's in an
Operation
subclass,isCancelled
is implicitly referencing itself rather than some ivar, avoiding any confusion about what it's checking. And your "start a new query" code can just say "cancel anything currently on the the relevant operation queue and add a new operation onto that queue":private var searchQueue: OperationQueue = {
let queue = OperationQueue()
// queue.maxConcurrentOperationCount = 1 // make it serial if you want
queue.name = Bundle.main.bundleIdentifier! + ".backgroundQueue"
return queue
}()
func performSearch(for queryString: String) {
searchQueue.cancelAllOperations()
let operation = SearchOperation(queryString: queryString)
searchQueue.addOperation(operation)
}I recommend this approach as you end up with a small cohesive object, the operation, that nicely encapsulates a block of work that you want to do, in the spirit of the Single Responsibility Principle.
While the following is less elegant, technically you can also use
BlockOperation
, which is block-based, but for which which you can decouple the creation of the operation, and the adding of the closure to the operation. Using this technique, you can actually pass a reference to the operation to its own closure:private weak var lastOperation: Operation?
func performSearch(for queryString: String) {
lastOperation?.cancel()
let operation = BlockOperation()
operation.addExecutionBlock { [weak operation, weak self] in
print("starting \(identifier)")
for i in 0 ... 10 {
if operation?.isCancelled ?? true { print("canceled \(identifier)"); return }
print(" \(identifier): \(i)")
self?.heavyWork()
}
print("finished \(identifier)")
}
searchQueue.addOperation(operation)
lastOperation = operation
}
func heavyWork() {
Thread.sleep(forTimeInterval: 0.5)
}I only mention this for the sake of completeness. I think the
Operation
subclass approach is frequently a better design. I'll useBlockOperation
for one-off sort of stuff, but as soon as I want more sophisticated cancelation logic, I think theOperation
subclass approach is better.
I should also mention that, in addition to more elegant cancelation capabilities, Operation
objects offer all sorts of other sophisticated capabilities (e.g. asynchronously manage queue of tasks that are, themselves, asynchronous; constrain degree of concurrency; etc.). This is all beyond the scope of this question.
Related Topics
Overriding Superclass Property With Different Type in Swift
The "++" and "--" Operators Have Been Deprecated Xcode 7.3
Unexpected Non-Void Return Value in Void Function (Swift 2.0)
Nsuserdefaults Not Working on Xcode Beta With Watch Os2
Arkit - What Do the Different Columns in Transform Matrix Represent
How to Convert a Decimal Number to Binary in Swift
Fatal Error: Swapping a Location With Itself Is Not Supported With Swift 2.0
Knowing Where Retain Cycles Are and Removing Them
Setting Device Orientation in Swift Ios
How to Print Double Quotes Inside ""
Multiple Functions With the Same Name
Ios 14 Swiftui Keyboard Lifts View Automatically
Decrement Index in a Loop After Swift C-Style Loops Deprecated
Calculate Age from Birth Date Using Nsdatecomponents in Swift