How to Stop a Dispatchworkitem in Gcd

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 the init method, and performing the work in the main 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 use BlockOperation for one-off sort of stuff, but as soon as I want more sophisticated cancelation logic, I think the Operation 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



Leave a reply



Submit