Search code examples
iosswiftmacosoperationqueue

Waiting on only one operation in an OperationQueue


When using OperationQueues in Swift, what's the best-practise way to wait until any operation in the queue has completed, as opposed to the usual requirement of waiting until all have completed via waitUntilAllOperationsAreFinished()?

The motivation is a concurrent search algorithm, in which any one of the search operations has an equal chance of finding the answer, at which point I want to cancel all of the other now-redundant operations which are looking for the same thing.

My initial naive implementation is to pass each operation a reference to the queue to allow the first to finish to call cancelAllOperations() on it, resulting in something of the following form:

class FindOperation: Operation {
    private weak var queue: OperationQueue?
    private var answerFound = false
    override var isFinished: Bool { answerFound }

    init(queue: OperationQueue) {
        self.queue = queue
    }

    override func start() {
        while(!answerFound && !self.isCancelled) {
            // Do some intensive work to find our answer
        }
        
        // We've found our answer so tell the queue to cancel other
        // operations, which were all looking for the same thing
        queue?.cancelAllOperations()
    }
}

...

let queue = OperationQueue()
let coreCount = ProcessInfo.processInfo.activeProcessorCount
queue.maxConcurrentOperationCount = coreCount

for _ in 0..<coreCount {
    let findOperation = FindOperation(queue: queue)
    queue.addOperation(findOperation)
}

queue.waitUntilAllOperationsAreFinished()

This feels wrong since operations definitely shouldn't need to know about their own queue.

I can find no reference in the OperationQueue docs that address this scenario. Is there a nicer way to wait for only one operation to complete?


Solution

  • The caller can set the completionBlock for the operations. It keeps all of the code associated with operation queues all in one area in the broader codebase.

    let queue = OperationQueue()
    
    let operations = (0..<count).map { _ in
        let operation = FindOperation()
        operation.completionBlock = { queue.cancelAllOperations() }
        return operation
    }
    
    queue.addOperations(operations, waitUntilFinished: true)
    

    Note, I mitigate the race between the adding of the operations and their cancelation by (a) creating all of the operations first; and then (b) adding them to the queue only after the operations (and their respective completionBlock closures) have been set.

    Also, remember, in synchronous operations, like you have here, one overrides main, not start. We only override start when writing an operation that is, itself, wrapping an asynchronous task (and then, of course, you also need to do all KVO for isExecuting, isFinished, etc.).

    Finally, I’ve set waitUntilFinished, because that’s what you’ve done in you code snippet, but one obviously never waits from the main thread. Perhaps you dispatched this entire code block to a background queue, but omitted that from your question?