问题描述
我正在做一些冗长的计算,以便在后台线程上创建图表数据
Im doing some lengthy calculations to create chart data on a background thread
i最初使用GCD,但是每次用户通过单击按钮来过滤统计图数据时,都需要重新计算统计图数据,如果用户非常快地单击统计图数据过滤按钮(高级用户),则统计图会循环每个GCD分派异步完成时,通过每个图进行浏览
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
我意识到我无法使用GCD取消线程,所以我已经转向尝试实现OperationQueue
I realize that I can't cancel threads with GCD so I've moved to trying to implement an OperationQueue
在向队列添加新操作之前,我先呼叫cancelAllOperations()
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.
我也很难取消正在执行的操作,因为当我在操作完成块中检查该操作的.isCancelled属性永远不会为真时
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)
}
推荐答案
顺便说一句,这并不完全正确.您可以取消调度到GCD队列中的DispatchWorkItem
个项目:
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()
}
诚然,您必须取消单个DispatchWorkItem
实例,但是它确实可以工作.
Admittedly, you have to cancel individual DispatchWorkItem
instances, but it does work.
不幸的是,这没有正确实现.简而言之,问题中的代码正在创建一个操作,该操作本身在操作主体中不执行任何操作,而是在其完成处理程序中包含所有计算量大的代码.但是,仅在操作完成"之后才调用此完成处理程序.并且已完成的操作(即那些已经在运行其完成处理程序的操作)无法取消.因此,该操作将忽略取消这些正在进行的,耗时的完成处理程序块的尝试.
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()
}
或者甚至更好的方法是创建一个Operation
子类来完成这项工作. Operation
和OperationQueue
的优点之一是您可以从视图控制器代码中解开复杂的操作代码.
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.
例如:
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
}
}
}
我不知道您的activeFeeds
是什么,因此我将其声明为Feed
的数组,但可以根据需要进行调整.但这说明了同步操作的思想:只需将Operation
子类化并添加main
方法.如果要将数据传递给操作,请将其作为参数添加到init
方法中.如果要传回数据,请添加一个闭包参数,该参数将在操作完成时被调用.请注意,我更喜欢依赖于内置的completionHandler
,因为它没有像上面的自定义完成处理程序那样提供将参数传递给闭包的机会.
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.
顺便说一句,虽然我展示了如何确保操作可取消,但您可能还需要确保在各个for
循环中检查isCancelled
(或者可能只是嵌套最深的循环).实际上,您正在过程中的早期检查isCancelled
,如果以后不进行检查,它将忽略随后的取消.调度和操作队列不会执行抢先取消,因此您必须在希望识别出取消的任何点插入isCancelled
检查.
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.
这篇关于无法取消OperationQueue swift中正在执行的操作的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!