iosswiftasynchronousswift-concurrencystructured-concurrency

Running a function in a background thread in Swift IOS


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
    }
}

Solution

  • 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. sleeping threads is not something you should do in real code.