Si j'ai une file d'attente série, comment puis-je, à partir du thread principal, lui dire d'arrêter immédiatement l'exécution et d'annuler toutes ses tâches?
Il n'y a aucun moyen de vider les tâches en attente d'une file d'attente de répartition sans implémenter vous-même une logique non triviale à partir d'iOS 9/OS X 10.11.
Si vous avez besoin d'annuler une file d'attente de répartition, vous feriez mieux d'utiliser NSOperationQueue
qui offre cela et plus encore. Par exemple, voici comment "annuler" une file d'attente:
NSOperationQueue* queue = [NSOperationQueue new];
queue.maxConcurrentOperationCount = 1; // make it a serial queue
...
[queue addOperationWithBlock:...]; // add operations to it
...
// Cleanup logic. At this point _do not_ add more operations to the queue
queue.suspended = YES; // halts execution of the queue
[queue cancelAllOperations]; // notify all pending operations to terminate
queue.suspended = NO; // let it go.
queue=nil; // discard object
C'est une question assez courante à laquelle j'ai déjà répondu:
Suspension du problème de requête GCD
La réponse courte est que GCD n'a pas d'API d'annulation; vous devez implémenter vous-même votre code d'annulation. Dans ma réponse, ci-dessus, je montre essentiellement comment cela peut être fait.
Si vous utilisez Swift
la classe DispatchWorkItem
permet d'annuler les unités works individuellement.
Les éléments de travail vous permettent de configurer directement les propriétés des unités de travail individuelles. Ils vous permettent également de vous adresser à des unités de travail individuelles dans le but d'attendre leur achèvement, d'être informé de leur achèvement et/ou de les annuler. (disponible pour une utilisation dans iOS 8.0+ macOS 10.10+).
DispatchWorkItem encapsule le travail qui peut être effectué. Un élément de travail peut être envoyé sur une DispatchQueue et au sein d'un DispatchGroup. Un DispatchWorkItem peut également être défini comme un événement DispatchSource, un enregistrement ou un gestionnaire d'annulation.
↳ https://developer.Apple.com/reference/dispatch/dispatchworkitem
Je ne sais pas si vous pouvez arrêter un bloc en cours d'exécution, mais vous pouvez appeler dispatch_suspend pour empêcher la file d'attente d'exécuter de nouveaux éléments de file d'attente. Vous pouvez ensuite appeler dispatch_resume pour redémarrer l'exécution (mais cela ne sonne pas comme si c'était ce que vous vouliez faire).
L'annulation d'un objet opération laisse l'objet dans la file d'attente mais informe l'objet qu'il doit arrêter sa tâche le plus rapidement possible. Pour les opérations en cours d'exécution, cela signifie que le code de travail de l'objet d'opération doit vérifier l'état d'annulation, arrêter ce qu'il fait et se marquer comme terminé.
Solution
class ViewController: UIViewController {
private lazy var queue = OperationQueue()
override func viewDidLoad() {
super.viewDidLoad()
queue.addOperation(SimpleOperation(title: "Task1", counter: 50, delayInUsec: 100_000))
queue.addOperation(SimpleOperation(title: "Task2", counter: 10, delayInUsec: 500_000))
DispatchQueue .global(qos: .background)
.asyncAfter(deadline: .now() + .seconds(3)) { [weak self] in
guard let self = self else { return }
self.queue.cancelAllOperations()
print("Cancel tasks")
}
}
}
class SimpleOperation: Operation {
private let title: String
private var counter: Int
private let delayInUsec: useconds_t
init(title: String, counter: Int, delayInUsec: useconds_t) {
self.title = title
self.counter = counter
self.delayInUsec = delayInUsec
}
override func main() {
if isCancelled { return }
while counter > 0 {
print("\(title), counter: \(counter)")
counter -= 1
usleep(delayInUsec)
if isCancelled { return }
}
}
}
Solution
protocol DispatchWorkItemControllerDelegate: class {
func workСompleted(delegatedFrom controller: DispatchWorkItemController)
}
class DispatchWorkItemController {
weak var delegate: DispatchWorkItemControllerDelegate?
private(set) var workItem: DispatchWorkItem?
private var semaphore = DispatchSemaphore(value: 1)
var needToStop: Bool {
get {
semaphore.wait(); defer { semaphore.signal() }
return workItem?.isCancelled ?? true
}
}
init (block: @escaping (_ needToStop: ()->Bool) -> Void) {
let workItem = DispatchWorkItem { [weak self] in
block { return self?.needToStop ?? true }
}
self.workItem = workItem
workItem.notify(queue: DispatchQueue.global(qos: .utility)) { [weak self] in
guard let self = self else { return }
self.semaphore.wait(); defer { self.semaphore.signal() }
self.workItem = nil
self.delegate?.workСompleted(delegatedFrom: self)
}
}
func setNeedsStop() { workItem?.cancel() }
func setNeedsStopAndWait() { setNeedsStop(); workItem?.wait() }
}
Utilisation de la solution de base (échantillon complet)
class ViewController: UIViewController {
lazy var workItemController1 = { self.createWorkItemController(title: "Task1", counter: 50, delayInUsec: 100_000) }()
lazy var workItemController2 = { self.createWorkItemController(title: "Task2", counter: 10, delayInUsec: 500_000) }()
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.global(qos: .default).async(execute: workItemController1.workItem!)
DispatchQueue.global(qos: .default).async(execute: workItemController2.workItem!)
DispatchQueue .global(qos: .background)
.asyncAfter(deadline: .now() + .seconds(3)) { [weak self] in
guard let self = self else { return }
self.workItemController1.setNeedsStop()
self.workItemController2.setNeedsStop()
print("tasks canceled")
}
}
private func createWorkItemController(title: String, counter: Int, delayInUsec: useconds_t) -> DispatchWorkItemController {
let controller = DispatchWorkItemController { needToStop in
var counter = counter
while counter > 0 {
print("\(title), counter: \(counter)")
counter -= 1
usleep(delayInUsec)
if needToStop() { print("canceled"); return }
}
}
controller.delegate = self
return controller
}
}
extension ViewController: DispatchWorkItemControllerDelegate {
func workСompleted(delegatedFrom controller: DispatchWorkItemController) {
print("-- work completed")
}
}
ajoutez le code de DispatchWorkItemController ici
protocol QueueControllerDelegate: class {
func tasksСompleted(delegatedFrom controller: QueueController)
}
class QueueController {
weak var delegate: QueueControllerDelegate?
private var queue: DispatchQueue
private var workItemControllers = [DispatchWorkItemController]()
private var semaphore = DispatchSemaphore(value: 1)
var runningTasksCount: Int {
semaphore.wait(); defer { semaphore.signal() }
return workItemControllers.filter { $0.workItem != nil } .count
}
func setNeedsStopTasks() {
semaphore.wait(); defer { semaphore.signal() }
workItemControllers.forEach { $0.setNeedsStop() }
}
func setNeedsStopTasksAndWait() {
semaphore.wait(); defer { semaphore.signal() }
workItemControllers.forEach { $0.setNeedsStopAndWait() }
}
init(queue: DispatchQueue) { self.queue = queue }
func async(block: @escaping (_ needToStop: ()->Bool) -> Void) {
queue.async(execute: initWorkItem(block: block))
}
private func initWorkItem(block: @escaping (_ needToStop: ()->Bool) -> Void) -> DispatchWorkItem {
semaphore.wait(); defer { semaphore.signal() }
workItemControllers = workItemControllers.filter { $0.workItem != nil }
let workItemController = DispatchWorkItemController(block: block)
workItemController.delegate = self
workItemControllers.append(workItemController)
return workItemController.workItem!
}
}
extension QueueController: DispatchWorkItemControllerDelegate {
func workСompleted(delegatedFrom controller: DispatchWorkItemController) {
semaphore.wait(); defer { semaphore.signal() }
if let index = self.workItemControllers.firstIndex (where: { $0.workItem === controller.workItem }) {
workItemControllers.remove(at: index)
}
if workItemControllers.isEmpty { delegate?.tasksСompleted(delegatedFrom: self) }
}
}
Utilisation de QueueController (exemple complet)
class ViewController: UIViewController {
let queue = QueueController(queue: DispatchQueue(label: "queue", qos: .utility,
attributes: [.concurrent],
autoreleaseFrequency: .workItem,
target: nil))
override func viewDidLoad() {
super.viewDidLoad()
queue.delegate = self
runTestLoop(title: "Task1", counter: 50, delayInUsec: 100_000)
runTestLoop(title: "Task2", counter: 10, delayInUsec: 500_000)
DispatchQueue .global(qos: .background)
.asyncAfter(deadline: .now() + .seconds(3)) { [weak self] in
guard let self = self else { return }
print("Running tasks count: \(self.queue.runningTasksCount)")
self.queue.setNeedsStopTasksAndWait()
print("Running tasks count: \(self.queue.runningTasksCount)")
}
}
private func runTestLoop(title: String, counter: Int, delayInUsec: useconds_t) {
queue.async { needToStop in
var counter = counter
while counter > 0 {
print("\(title), counter: \(counter)")
counter -= 1
usleep(delayInUsec)
if needToStop() { print("-- \(title) canceled"); return }
}
}
}
}
extension ViewController: QueueControllerDelegate {
func tasksСompleted(delegatedFrom controller: QueueController) {
print("-- all tasks completed")
}
}
Voir cancelAllOperations sur NSOperationQueue. C'est toujours à vous de vous assurer que vos opérations gèrent correctement le message d'annulation.
Une autre solution consiste à jeter l'ancienne file d'attente et à en créer une nouvelle. Ça marche pour moi. C'est comme supprimer un tableau, vous pouvez supprimer tous les éléments sur celui-ci ou vous pouvez simplement en créer un nouveau pour remplacer l'ancien.