Как получить состояние отмены для нескольких DispatchWorkItems - PullRequest
0 голосов
/ 13 декабря 2018

Фон

Я выполняю поиск.Каждый поисковый запрос приводит к одному 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 только один раз, я мог бы просто получить доступ к этому атрибуту, как в случае с в этом случае .Однако это здесь не применимо, поскольку атрибут будет переопределен до завершения метода filter(). Как узнать, какой экземпляр действительно выполняет код, в котором я хочу проверить dispatchWorkItem.isCancelled?

В идеале я хотел бы предоставить недавно созданный DispatchWorkItem какдополнительный параметр к методу filter().Но это невозможно, потому что я получу ошибку Variable used within its own initial value.

Я новичок в Swift, поэтому я надеюсь, что просто что-то упустил.Любая помощь очень ценится!

Ответы [ 2 ]

0 голосов
/ 22 декабря 2018

Хитрость заключается в том, как проверить отправленную задачу, если она была отменена.Я бы на самом деле предложил рассмотреть OperationQueue подход, а не использовать очереди отправки напрямую.

Существует как минимум два подхода:

  • Большинствоэлегантно, IMHO, просто подкласс Operation, передавая ему все, что вы хотите в методе init, и выполняя работу в методе main:

    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 неявно ссылается на себя, а не на какой-то ивар, избегая путаницы в том, что он проверяет.А ваш код «начать новый запрос» может просто сказать «отменить все, что в данный момент находится в соответствующей очереди операций, и добавить новую операцию в эту очередь»:

    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 лучше.

IСледует также отметить, что, в дополнение к более элегантным возможностям отмены, объекты Operation предлагают все виды других сложных возможностей (например, асинхронное управление очередью задач, которые сами по себе являются асинхронными; ограниченная степень параллелизма и т. д.).Все это выходит за рамки этого вопроса.

0 голосов
/ 18 декабря 2018

вы написали

В идеале, я хотел бы предоставить вновь созданный 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

разница в том, что первые два исполнения никогда не происходили.(были удалены из очереди отправки перед выполнением)

...