Can't cancel executing operations in OperationQueue swift

被刻印的时光 ゝ 提交于 2019-12-05 12:06:15

I realize that I can't cancel threads with GCD ...

Just as an aside, that's not entirely true. You can cancel DispatchWorkItem items dispatched to a GCD queue:

var item: DispatchWorkItem!
item = DispatchWorkItem {
    ...

    while notYetDone() {
        if item.isCancelled {
            os_log("canceled")
            return
        }

        ...
    }

    os_log("finished")
}

let queue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".customQueue")

queue.async(execute: item)

// just to prove it's cancelable, let's cancel it one second later

DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
    os_log("canceling")
    item.cancel()
}

Admittedly, you have to cancel individual DispatchWorkItem instances, but it does work.

... so I've moved to trying to implement an OperationQueue

Unfortunately, this has not been implemented correctly. In short, the code in your question is creating an operation that does nothing in the body of the operation itself, but instead has all of the computationally intensive code in its completion handler. But this completion handler is only called after the operation is “completed”. And completed operations (ie., those already running their completion handlers) cannot be canceled. Thus, the operation will ignore attempts to cancel these ongoing, time-consuming completion handler blocks.

Instead, create an block operation, and add your logic as a "execution block", not a completion handler. Then cancelation works as expected:

let operation = BlockOperation()
operation.addExecutionBlock {
    ...

    while notYetDone() {
        if operation.isCancelled {
            os_log("canceled")
            return
        }

        ...
    }

    os_log("finished")
}

let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1

queue.addOperation(operation)

// just to prove it's cancelable, let's cancel it

DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
    os_log("canceling")
    operation.cancel()
}

Or, perhaps even better, create an Operation subclass that does this work. One of the advantages of Operation and OperationQueue has that you can disentangle the complicated operation code from the view controller code.

For example:

class ChartOperation: Operation {

    var feeds: [Feed]
    private var chartOperationCompletion: (([IChartDataSet]?) -> Void)?

    init(feeds: [Feed], completion: (([IChartDataSet]?) -> Void)? = nil) {
        self.feeds = feeds
        self.chartOperationCompletion = completion
        super.init()
    }

    override func main() {
        let results = [IChartDataSet]()

        while notYetDone() {
            if isCancelled {
                OperationQueue.main.addOperation {
                    self.chartOperationCompletion?(nil)
                    self.chartOperationCompletion = nil
                }
                return
            }

            ...
        }

        OperationQueue.main.addOperation {
            self.chartOperationCompletion?(results)
            self.chartOperationCompletion = nil
        }
    }

}

I didn't know what your activeFeeds was, so I declared it as an array of Feed, but adjust as you see fit. But it illustrates the idea for synchronous operations: Just subclass Operation and add a main method. If you want to pass data to the operation, add that as a parameter to the init method. If you want to pass data back, add a closure parameter which will be called when the operation is done. Note, I prefer this to relying on the built-in completionHandler because that doesn't offer the opportunity to supply parameters to be passed to the closure like the above custom completion handler does.

Anyway, your view controller can do something like:

let operation = ChartOperation(feeds: activeFeeds) { results in
    // update UI here
}

queue.addOperation(operation)

And this, like the examples above, is cancelable.


By the way, while I show how to ensure the operation is cancelable, you may also want to make sure you're checking isCancelled inside your various for loops (or perhaps just at the most deeply nested for loop). As it is, you're checking isCancelled early in the process, and if you don't check it later, it will ignore subsequent cancelations. Dispatch and operation queues do not perform preemptive cancelations, so you have to insert your isCancelled checks at whatever points you'd like cancelations to be recognized.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!