Im doing some lengthy calculations to create chart data on a background thread
i was originally use GCD, but every time a user filters the chart data by hitting a button, the chart data needs to be recalculated, if the user clicks the chart data filtering buttons very quickly (power user) then the chart loops through each drawing as each GCD dispatch async finishes
I realize that I can't cancel threads with GCD so I've moved to trying to implement an OperationQueue
I call cancelAllOperations()
before adding a new operation to the queue
The operations on the queue act funky, sometimes it seems like they are cancelled, some times it seems like the one that finished is not the most recent one put on the queue.
I am also having trouble cancelling a executing operation as the operation's .isCancelled property is never true when i check for it in the operations completion block
What i really want is if the chart data calculation is currently happening in a background thread, and a user clicks another filter button and kicks off another chart calculation on a background thread, the previous chart background thread calculation is terminated and "replaced" with the most recently added operation
is this possible? here is some code:
func setHistoricalChart() -> Void {
self.lineChartView.clear()
self.lineChartView.noDataText = "Calculating Historical Totals, Please Wait..."
self.historicalOperationsQueue.qualityOfService = .utility
self.historicalOperationsQueue.maxConcurrentOperationCount = 1
self.historicalOperationsQueue.name = "historical operations queue"
let historicalOperation = Operation()
historicalOperation.completionBlock = { [weak self] in
//dictionary of feeds, array of data for each feed
var valuesByFeed = [String:[String]?]()
var dates = [String:[String]?]()
var chartDataSets = [IChartDataSet]()
//get data and values from DataMOs in the activeFeeds
if (self?.activeFeeds.count)! > 0 {
//check if operation is cancelled
if historicalOperation.isCancelled {
return
}
for (key, feed) in (self?.activeFeeds)! {
dates[key] = feed?.datas?.flatMap({ Utils.formatUTCDateString(utcDateString: ($0 as! DataMO).utcDateString) })
valuesByFeed[key] = feed?.datas?
.sorted(by: { (($0 as! DataMO).utcDateString)! < (($1 as! DataMO).utcDateString)! })
.flatMap({ ($0 as! DataMO).value })
}
//Create Chart Data
for (key, valuesArray) in valuesByFeed {
var dataEntries = [ChartDataEntry]()
for (index, value) in (valuesArray?.enumerated())! {
let dataEntry = ChartDataEntry(x: Double(index), y: Double(value)!)
dataEntries.append(dataEntry)
}
let singleChartDataSet = LineChartDataSet(values: dataEntries, label: key)
singleChartDataSet.drawCirclesEnabled = false
switch key {
case "Solar":
singleChartDataSet.setColors(UIColor(red: 230/255, green: 168/255, blue: 46/255, alpha: 1))
singleChartDataSet.drawFilledEnabled = true
singleChartDataSet.fillColor = UIColor(red: 230/255, green: 168/255, blue: 46/255, alpha: 0.8)
break
case "Wind":
singleChartDataSet.setColors(UIColor(red: 73/255, green: 144/255, blue: 226/255, alpha: 1))
singleChartDataSet.drawFilledEnabled = true
singleChartDataSet.fillColor = UIColor(red: 73/255, green: 144/255, blue: 226/255, alpha: 0.8)
break
case "Battery":
singleChartDataSet.setColors(UIColor(red: 126/255, green: 211/255, blue: 33/255, alpha: 1))
singleChartDataSet.drawFilledEnabled = true
singleChartDataSet.fillColor = UIColor(red: 126/255, green: 211/255, blue: 33/255, alpha: 0.8)
break
case "Gen":
singleChartDataSet.setColors(UIColor(red: 208/255, green: 1/255, blue: 27/255, alpha: 1))
singleChartDataSet.drawFilledEnabled = true
singleChartDataSet.fillColor = UIColor(red: 208/255, green: 1/255, blue: 27/255, alpha: 0.8)
break
case "Demand":
singleChartDataSet.setColors(UIColor(red: 128/255, green: 133/255, blue: 233/255, alpha: 1))
singleChartDataSet.drawFilledEnabled = true
singleChartDataSet.fillColor = UIColor(red: 128/255, green: 133/255, blue: 233/255, alpha: 0.8)
break
case "Prod":
singleChartDataSet.setColors(UIColor(red: 241/255, green: 92/255, blue: 128/255, alpha: 1))
singleChartDataSet.drawFilledEnabled = true
singleChartDataSet.fillColor = UIColor(red: 241/255, green: 92/255, blue: 128/255, alpha: 0.8)
break
default:
break
}
chartDataSets.append(singleChartDataSet)
}
}
//check if operation is cancelled
if historicalOperation.isCancelled {
return
}
//set chart data
let chartData = LineChartData(dataSets: chartDataSets)
//update UI on MainThread
OperationQueue.main.addOperation({
if (self?.activeFeeds.count)! > 0 {
self?.lineChartView.data = chartData
} else {
self?.lineChartView.clear()
self?.lineChartView.noDataText = "No Feeds To Show"
}
})
}
historicalOperationsQueue.cancelAllOperations()
historicalOperationsQueue.addOperation(historicalOperation)
}
you can call op2. cancel() to cancel the operation, but you need to take additional steps to really stop your operation from running as cancel() only set the isCanceled property to true. The default value of this property is false. Calling the cancel() method of this object sets the value of this property to true.
The interface to cancel an operation is quite simple. If you just want to cancel a specific Operation , then you can call the cancel method. If, on the other hand, you wish to cancel all operations that are in an operation queue, then you should call the cancelAllOperations method defined on OperationQueue .
suspend() when you send the operation, and have the code that gets the response call . resume() . Then wherever you want to wait for the response before continuing, just put a dummy queue. sync({ print("done waiting")}) and it will automatically wait until .
An operation queue invokes its queued Operation objects based on their priority and readiness. After you add an operation to a queue, it remains in the queue until the operation finishes its task. You can't directly remove an operation from a queue after you add it. Note.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With