【问题标题】:Can't cancel executing operations in OperationQueue swift无法在 OperationQueue swift 中取消执行操作
【发布时间】:2017-07-28 20:28:50
【问题描述】:

我正在做一些冗长的计算以在后台线程上创建图表数据

我最初是使用GCD,但是每次用户通过点击按钮过滤图表数据时,都需要重新计算图表数据,如果用户非常快速地点击图表数据过滤按钮(高级用户)然后图表循环在每个 GCD 调度异步完成时通过每个绘图

我意识到我无法使用 GCD 取消线程,所以我开始尝试实现 OperationQueue

在向队列添加新操作之前,我会调用cancelAllOperations()

队列上的操作很奇怪,有时看起来像是被取消了,有时看起来完成的不是最近放入队列的操作。

我也无法取消正在执行的操作,因为当我在操作完成块中检查该操作的 .isCancelled 属性时,它永远不会为真

我真正想要的是,如果图表数据计算当前正在后台线程中进行,并且用户单击另一个过滤器按钮并在后台线程上启动另一个图表计算,则之前的图表后台线程计算被终止并“替换" 带有最近添加的操作

这可能吗? 这是一些代码:

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)
}

【问题讨论】:

  • 取消操作容易受到竞争条件的影响,您无法避免这种情况。在更新 GUI 或任何非本地状态变量之前,在操作开始时安排所有耗时的调用可能对您很有帮助。您的操作中最耗时的功能是什么?
  • 我还推荐 WWDC 2015 的 Advanced NSOperations。在 5:00 左右讨论取消
  • 问题是每次用户点击按钮都会调用整个函数,我想避免用户快速点击按钮三次,然后图表缓慢更新三次,似乎这样做时对用户很迟钝
  • 然后在更新进行时禁用刷新按钮更有意义,不是吗?
  • 我添加了一个屏幕截图,你可以看到用户可以点击一个按钮来添加或删除图表上的数据点,它是发生在 .utility 队列上的图表数据计算。您真的认为在后台线程上开始计算图表数据时禁用数据过滤按钮是最好的用户体验吗?这是常见的 iOS 用户体验实践吗?

标签: ios swift multithreading grand-central-dispatch nsoperationqueue


【解决方案1】:

我意识到我无法使用 GCD 取消线程...

顺便说一句,这并不完全正确。您可以取消分配到 GCD 队列的 DispatchWorkItem 项:

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 实例,但它确实有效。

...所以我开始尝试实现OperationQueue

很遗憾,这并未正确实施。简而言之,您问题中的代码正在创建一个操作,该操作在操作本身的主体中不执行任何操作,而是在其完成处理程序中包含所有计算密集型代码。但是这个完成处理程序只有在操作“完成”后才会被调用。并且已完成的操作(即那些已经在运行其完成处理程序的操作)不能被取消。因此,该操作将忽略取消这些正在进行的、耗时的完成处理程序块的尝试。

相反,创建一个块操作,并将您的逻辑添加为“执行块”,而不是完成处理程序。然后取消按预期工作:

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 子类。 OperationOperationQueue 的优点之一是可以将复杂的操作代码从视图控制器代码中分离出来。

例如:

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,因为它不能像上面的自定义完成处理程序那样提供要传递给闭包的参数。

无论如何,您的视图控制器可以执行以下操作:

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

queue.addOperation(operation)

而且,和上面的例子一样,这个是可以取消的。


顺便说一下,虽然我展示了如何确保操作是可取消的,但您可能还想确保在各种 for 循环中检查 isCancelled(或者可能只是在最深层嵌套的 @987654339 @ 环形)。事实上,您在此过程的早期检查isCancelled,如果您稍后不检查,它将忽略随后的取消。调度和操作队列不执行抢先取消,因此您必须在您希望识别取消的任何点插入isCancelled 检查。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-07-13
    • 2019-06-12
    • 1970-01-01
    • 2017-06-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多