I am a beginner to swift and ios development and need some help with structured concurrency. I need a long running function to execute on a background thread, and be cancelled if it is called again before completing. Unfortunately, my code allows the 2nd call to start before the currently running task finishes cancelling. Can someone provide an example of how to do this properly?
I removed the "guts" from my code and just left the concurrency structure in place. The function "doLongWork()" is called from the UI, and it runs the function "longWork()" in the background after cancelling the previous task if one exists.
class SomeClass {
var workItem: DispatchWorkItem?
var queue1 = DispatchQueue(label: “serialQueue”))
func doLongWork() {
if workItem != nil {
queue1.async(flags: .barrier) { /* barrier */ }
workItem?.cancel()
}
workItem = DispatchWorkItem { self.longWork() }
queue1.async{ self.workItem?.perform() }
}
func longWork() {
func recursiveFunction() {
if let cancelled = workItem?.isCancelled {
if cancelled { return }
}
// calculations
}
recursiveFunction()
if let cancelled = workItem?.isCancelled {
if cancelled {return}
}
OperationQueue.main.addOperation {
// update ui
}
workItem = nil
}
}
First, your code isn't using Swift concurrency. It is using a Grand Central Dispatch DispatchQueue
.
You have a few issues with your code.
The first is that you are explicitly calling perform
on your work item. You don't need to do this. You can simply submit the DispatchWorkItem
to the DispatchQueue
and it will be executed.
The bigger issue is that you are using a single property, workItem
to hold a reference to your DispatchWorkItem
.
In doLongWork
you call workItem?.cancel()
and then store a new DispatchWorkItem
in the workItem
property, but DispatchWorkItems
are not cancelled pre-emptively. Your code needs to periodically check the state of isCancelled
. When it references workItem
it will be seeing the new DispatchWorkItem
, not the item that represents 'itself'.
The solution is to capture the workItem
property in a local variable inside the longWork
function. Then, no matter what happens to the workItem
property, the code can see whether it has been cancelled.
class WorkManager {
private var workItem: DispatchWorkItem?
private var queue1 = DispatchQueue(label: "serialQueue")
func doLongWork() {
self.workItem?.cancel()
let newWorkItem = DispatchWorkItem {
longWork()
}
self.workItem = newWorkItem
queue1.async(execute: newWorkItem)
func longWork() {
guard let workItem else {
print("workItem is nil")
return
}
var cancelled: Bool {
return workItem.isCancelled
}
print("Starting work")
recursiveFunction()
func recursiveFunction(depth: Int = 0) {
if depth > 5 {
print("Depth reached")
return
}
if cancelled {
print("cancelled")
return
}
print("Sleeping")
Thread.sleep(forTimeInterval: 1)
print("Wake up")
recursiveFunction(depth: depth + 1)
}
}
}
}
Note that I am using Thread.sleep()
to simulate a CPU intensive task. sleep
ing threads is not something you should do in real code.