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

Joh*_*145 4 concurrency grand-central-dispatch ios swift

背景

我正在实施搜索。每个搜索查询都会生成一个 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!)
}
Run Code Online (Sandbox Code Playgroud)

问题

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

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

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

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

Rob*_*Rob 6

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

至少有两种方法:

  • 恕我直言,最优雅的是只是子类化Operation,在方法中将您想要的任何内容传递给它init,并在方法中执行工作main

     class SearchOperation: Operation {
         private var queryString: String
    
         init(queryString: String) { 
             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)
         }
     }
    
    Run Code Online (Sandbox Code Playgroud)

    因为它位于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)
     }
    
    Run Code Online (Sandbox Code Playgroud)

    我推荐这种方法,因为您最终会得到一个小的内聚对象(操作),它本着单一职责原则的精神很好地封装了您想要执行的工作块。

  • 虽然以下内容不太优雅,但从技术上讲,您也可以使用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)
     }
    
    Run Code Online (Sandbox Code Playgroud)

    我只是为了完整起见才提到这一点。我认为Operation子类方法通常是更好的设计。我将用于BlockOperation一次性的东西,但一旦我想要更复杂的取消逻辑,我认为Operation子类方法更好。

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