If I have a serial queue, how can I, from the main thread, tell it to immediately stop execution and cancel all of its tasks?
If I have a serial queue, how can I, from the main thread, tell it to immediately stop execution and cancel all of its tasks?
If you're using Swift the DispatchWorkItem class allows works units to be cancelled individually.
Work items allow you to configure properties of individual units of work directly. They also allow you to address individual work units for the purposes of waiting for their completion, getting notified about their completion, and/or canceling them. ( available for use in iOS 8.0+ macOS 10.10+ ).
DispatchWorkItem encapsulates work that can be performed. A work item can be dispatched onto a DispatchQueue and within a DispatchGroup. A DispatchWorkItem can also be set as a DispatchSource event, registration, or cancel handler.
↳ https://developer.apple.com/reference/dispatch/dispatchworkitem
Canceling an operation object leaves the object in the queue but notifies the object that it should stop its task as quickly as possible. For currently executing operations, this means that the operation object’s work code must check the cancellation state, stop what it is doing, and mark itself as finished
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() }
}
Usage of base solution (full sample)
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")
}
}
add code of DispatchWorkItemController here
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) }
}
}
Usage of QueueController (full sample)
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")
}
}
I have found a fun solution to this type of problem when trying to solve my own similar issue. The basic concept is that whatever class calls the dispatch, it has a id property that tracks the current execution of some method, for me, it was opening an alert view. The method that calls the dispatch then holds a local variable of a generated id. If the id has not be changed, then I know not to cancel my callback. If it has been changed, then take no action because some other alert has taken control:
class AlertData: ObservableObject {
static var shared = AlertData()
@Published var alertOpen = false
@Published var alertMessage = ""
@Published var alertTitle = ""
var id: UUID = UUID()
func openAlert() {
// ID is used to only dismiss the most recent version of alert within timeout.
let myID = UUID()
self.id = myID
withAnimation {
self.alertOpen = true
}
DispatchQueue.main.asyncAfter(deadline: (.now() + 2), execute: {
// Only dismiss if another alert has not appeared and taken control
if self.id == myID {
withAnimation {
self.alertOpen = false
}
}
})
}
func closeAlert() {
withAnimation {
self.alertOpen = false
}
}
}
Was working around a similar problem earlier today where I wanted to abandon a task involved in loading the data for a view controller if the user were to navigate away before it finished. Basically, the approach I ended up settling on was to use weak references to the controller in the closure being executed by DispatchQueue and wrote the code to fail gracefully should it disappear.
My two cents: I use the below to cancel.
Create a dummy static variable:
static bool flushStatus;
Set the flush status, by using the above static variable as an unique key:
dispatch_queue_set_specific(queue, &flushStatus, (void*)1, NULL);
Within each of the queue block, use the static variable's value to get the flush status:
dispatch_async(queue, ^(){
if(dispatch_get_specific(&flushStatus))
return;
// Do your work here
});
Note that if block had already crossed the "dispatch_get_specific", you will not be able to stop from running.
The advantage is, it doesn't need to access any variable/object to know its cancellation state, just the queue - all by itself - is enough.
Edit: To address the above note - "Note that if block had already crossed the "dispatch_get_specific", you will not be able to stop from running.", one can place the dispatch_queue_set_specific(queue, &flushStatus, (void*)1, NULL); in a dispatch_barrier_sync, possibly in the destructor or in any terminating position