如何获取多个DispatchWorkItems的取消状态

时间:2018-12-13 12:50:07

标签: ios swift concurrency grand-central-dispatch

背景

我正在执行搜索。每个搜索查询都会生成一个DispatchWorkItem,然后将其排队等待执行。由于用户可以比上一次完成更快地触发新搜索,因此我想在收到新搜索后立即取消上一个搜索。

这是我当前的设置:

var currentSearchJob: DispatchWorkItem?
let searchJobQueue = DispatchQueue(label: QUEUE_KEY)

func updateSearchResults(for searchController: UISearchController) {
    let queryString = searchController.searchBar.text?.lowercased() ?? ""

    // if there is already an (older) search job running, cancel it
    currentSearchJob?.cancel()

    // create a new search job
    currentSearchJob = DispatchWorkItem() {
        self.filter(queryString: queryString)
    }

    // start the new job
    searchJobQueue.async(execute: currentSearchJob!)
}

问题

我知道dispatchWorkItem.cancel()不会立即终止正在运行的任务。相反,我需要手动检查dispatchWorkItem.isCancelled。但是在这种情况下如何获得正确的dispatchWorkItem对象?

如果只设置一次currentSearchJob,我可以像完成in this case那样简单地访问该属性。但是,这在这里不适用,因为在完成filter()方法之前,该属性将被覆盖。 我如何知道哪个实例实际在运行我要检查dispatchWorkItem.isCancelled的代码?

理想情况下,我想提供新创建的DispatchWorkItem作为filter()方法的附加参数。但这是不可能的,因为我会遇到Variable used within its own initial value错误。

我是Swift的新手,所以我希望我只是想念一些东西。非常感谢任何帮助!

2 个答案:

答案 0 :(得分:1)

诀窍是如何检查已取消的调度任务。我实际上建议您考虑使用OperationQueue方法,而不是直接使用调度队列。

至少有两种方法:

  • 最优雅的恕我直言,只是对Operation进行子类化,在init方法中传递您想要的任何内容,并在main方法中执行工作:< / p>

    class SearchOperation: Operation {
        private var queryString: String
    
        init(queryString: Int) { 
            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)
        }
    }
    

    因为它在Operation子类中,所以isCancelled隐式引用了自己而不是某些ivar,从而避免了对其检查内容的混淆。您的“开始新查询”代码只能说“取消相关操作队列上当前的任何内容,然后在该队列上添加新操作”:

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

    我建议您采用这种方法,因为最终会产生一个小的内聚对象,即操作,它按照“单一责任原则”的精神很好地封装了您要完成的工作。

  • 虽然以下内容不太美观,但从技术上讲,您也可以使用BlockOperation,它是基于块的,但是您可以将其分离以创建操作以及将闭包添加到操作。使用这种技术,您实际上可以将对该操作的引用传递给它自己的闭包:

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

    为了完整起见,我仅提及这一点。我认为Operation子类方法通常是一种更好的设计。我将使用BlockOperation处理一次性内容,但是一旦我想要更复杂的取消逻辑,我认为Operation子类方法会更好。

我还应该提到,Operation对象除了具有更优雅的取消功能外,还提供各种其他复杂的功能(例如,异步管理本身是异步的任务队列;限制并发程度;等等。 )。这超出了这个问题的范围。

答案 1 :(得分:-1)

您写了

  

理想情况下,我想将新创建的DispatchWorkItem作为   附加参数

您错了,要取消正在运行的任务,您需要引用它,而不是要准备下一个任务的引用。

cancel()不会取消正在运行的任务,它仅以线程安全的方式设置内部“ isCancel”标志,或者在执行之前将其从队列中删除。执行后,检查isCancel可以使您有机会完成工作(尽早返回)。

import PlaygroundSupport
import Foundation

PlaygroundPage.current.needsIndefiniteExecution = true

let queue = DispatchQueue.global(qos: .background)
let prq = DispatchQueue(label: "print.queue")
var task: DispatchWorkItem?

func work(task: DispatchWorkItem?) {
    sleep(1)
    var d = Date()
    if task?.isCancelled ?? true {
        prq.async {
            print("cancelled", d)
        }
        return
    }
    sleep(3)
    d = Date()
    prq.async {
        print("finished", d)
    }
}

for _ in 0..<3  {
    task?.cancel()
    let item = DispatchWorkItem {
        work(task: task)
    }
    item.notify(queue: prq) {
        print("done")
    }
    queue.asyncAfter(deadline: .now() + 0.5, execute: item)
    task = item
    sleep(1) // comment this line
}

在此示例中,只有最后一项工作才真正被完全执行

cancelled 2018-12-17 23:49:13 +0000
done
cancelled 2018-12-17 23:49:14 +0000
done
finished 2018-12-17 23:49:18 +0000
done

尝试注释最后一行并打印出来

done
done
finished 2018-12-18 00:07:28 +0000
done

区别在于,前两次执行从未发生。 (在执行前已从调度队列中删除)