如何将 NSOperation 安排到队列的头部并让所有其他操作等待它?

How to schedule a NSOperation to the head of queue and let all other operations waiting for it?

我有一个 NSOperationQueue 是并发的。对于特定的NSOperation,如果它失败了,我想立即以最高优先级重试这个操作,并暂停所有其他操作,直到它成功。

我可以考虑安排一个具有更高优先级的操作,但是我怎样才能让所有其他操作以一种有效的方式等待这个操作呢?更改所有剩余的操作依赖项似乎太耗时了。

有几种方法:

  1. 一种快速解决难题的简单方法是让可能需要多次尝试的任务在重试完成之前不完成(即,将重试登录合并到操作本身中) ).然后用屏障安排第一个任务,安排后续任务,这样 none 个后续任务将能够 运行 直到第一个任务完成(包括它的所有重试)。

  2. 或者,如果你想让重试任务单独操作,但又不想使用依赖关系,你可以将后续任务添加到一个单独的、暂停的队列中:

    let taskQueue = OperationQueue()
    taskQueue.maxConcurrentOperationCount = 4
    taskQueue.isSuspended = true
    
    for i in 0 ..< 20 {
        taskQueue.addOperation {
            ...
        }
    }
    

    然后,将可能需要重试的任务添加到另一个队列(即,显然是未挂起的队列):

    func attempt(_ count: Int = 0) {
        retryQueue.addOperation {
            ...
    
            if isSuccessful {
                taskQueue.isSuspended = false
            } else {
                attempt(count + 1)
            }
    
            ...
        }
    }
    

    执行此操作时,第一个操作将在满足必要条件时取消挂起任务队列:

  3. 为了完整起见,另一种选择是继承 Operation 并使 isReady 逻辑不仅 return 它的 super 实现,还要观察一些属性。例如

    class WaitingOperation: Operation {
        @objc dynamic var canStart = false
    
        var object: NSObject
        var observer: NSKeyValueObservation?
    
        let taskId: Int
    
        override var isReady: Bool { super.isReady && canStart }
    
        init<T>(object: T, canStartTasksKeyPath keyPath: KeyPath<T, Bool>, taskId: Int) where T: NSObject {
            self.object = object
            self.taskId = taskId
            super.init()
            observer = object.observe(keyPath, options: [.initial, .new]) { [weak self] _, changes in
                if let newValue = changes.newValue {
                    self?.canStart = newValue
                }
            }
        }
    
        override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
            var set = super.keyPathsForValuesAffectingValue(forKey: key)
    
            if key == #keyPath(isReady) {
                set.insert(#keyPath(canStart))
            }
    
            return set
        }
    
        override func main() {
            ...
        }
    }
    

    然后

    @objc dynamic var canStartTasks = false
    
    func begin() {
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 4
    
        for i in 0 ..< 20 {
            queue.addOperation(WaitingOperation(object: self, canStartTasksKeyPath: \.canStartTasks, taskId: i))
        }
    
        let start = CACurrentMediaTime()
        attempt()
    
        func attempt(_ count: Int = 0) {
            queue.addOperation { [self] in
                ...
    
                if notSuccessful {
                    attempt(count + 1)
                } else {
                    canStartTasks = true
                }
    
                ...
            }
        }
    }