n179911
n179911

Reputation: 20341

How to cancel an Asynchronous function in Swift

In swift, what is the common practice to cancel an aync execution?

Using this example, which execute the closure asynchronously, what is the way to cancel the async function?

func getSumOf(array:[Int], handler: @escaping ((Int)->Void)) {
    //step 2
    var sum: Int = 0
    for value in array {
        sum += value
    }
    //step 3
    Globals.delay(0.3, closure: {
        handler(sum)
    })
}

func doSomething() {
    //setp 1
    self.getSumOf(array: [16,756,442,6,23]) { [weak self](sum) in
        print(sum)
        //step 4, finishing the execution
    }
}
//Here we are calling the closure with the delay of 0.3 seconds
//It will print the sumof all the passed numbers.

Upvotes: 1

Views: 1962

Answers (3)

Rob
Rob

Reputation: 437897

Unfortunately, there is no generalized answer to this question as it depends entirely upon your asynchronous implementation.

Let's imagine that your delay was the typical naive implementation:

static func delay(_ timeInterval: TimeInterval, closure: @escaping () -> Void) {
    DispatchQueue.main.asyncAfter(deadline: .now() + timeInterval) {
        closure()
    }
}

That's not going to be cancelable.

However you can redefine it to use DispatchWorkItem. This is cancelable:

@discardableResult
static func delay(_ timeInterval: TimeInterval, closure: @escaping () -> Void) -> DispatchWorkItem {
    let task = DispatchWorkItem {
        closure()
    }
    
    DispatchQueue.main.asyncAfter(deadline: .now() + timeInterval, execute: task)
    
    return task
}

By making it return a @discardableResult, that means that you can use it like before, but if you want to cancel it, grab the result and pass it along. E.g., you can define your asynchronous sum routine to use this pattern, too:

@discardableResult
func sum(of array: [Int], handler: @escaping (Int) -> Void) -> DispatchWorkItem {
    let sum = array.reduce(0, +)

    return Globals.delay(3) {
        handler(sum)
    }
}

Now, doSomething can, if it wants, capture the returned value and use it to cancel the asynchronously scheduled task:

func doSomething() {
    var task = sum(of: [16, 756, 442, 6, 23]) { sum in
        print(Date(), sum)
    }
    
    ...

    task.cancel()
}

You can also implement the delay with a Timer:

@discardableResult
static func delay(_ timeInterval: TimeInterval, closure: @escaping () -> Void) -> Timer {
    Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: false) { _ in
        closure()
    }
}

And

@discardableResult
func sum(of array: [Int], handler: @escaping (Int) -> Void) -> Timer {
    let sum = array.reduce(0, +)

    return Globals.delay(3) {
        handler(sum)
    }
}

But this time, you'd invalidate the timer:

func doSomething() {
    weak var timer = sum(of: [16, 756, 442, 6, 23]) { sum in
        print(Date(), sum)
    }

    ...
    
    timer?.invalidate()
}

It must be noted that the above scenarios are unique to simple “delay” scenarios. This is not a general purpose solution for stopping asynchronous processes. For example, if the asynchronous tasks consists of some time consuming for loop, the above is insufficient.

For example, let's say you are doing something really complicated calculation in a for loop (e.g. processing the pixels of an image, processing frames of a video, etc.). In that case, because there is no preemptive cancelation, you'd need to manually check to see if the DispatchWorkItem or the Operation has been canceled by checking their respective isCancelled properties.

For example, let's consider an operation to sum all primes less than 1 million:

class SumPrimes: Operation {
    override func main() {
        var sum = 0
        
        for i in 1 ..< 1_000_000 {
            if isPrime(i) {
                sum += i
            }
        }
        
        print(Date(), sum)
    }
    
    func isPrime(_ value: Int) -> Bool { ... }   // this is slow
}

(Obviously, this isn't an efficient way to solve the “sum of primes less than x” problem, but it just an example for illustrative purposes.)

And

let queue = OperationQueue()
let operation = SumPrimes()
queue.addOperation(operation)

We're not going to be able to cancel that. Once it starts, there’s no stopping it.

But we can make it cancelable by adding a check for isCancelled in our loop:

class SumPrimes: Operation {
    override func main() {
        var sum = 0
        
        for i in 1 ..< 1_000_000 {
            if isCancelled { return }
            
            if isPrime(i) {
                sum += i
            }
        }
        
        print(Date(), sum)
    }
    
    func isPrime(_ value: Int) -> Bool { ... }
}

And

let queue = OperationQueue()
let operation = SumPrimes()
queue.addOperation(operation)

...

operation.cancel()

Bottom line, if it’s something other than a simple delay, and you want it to be cancelable, you have to integrate this into your code that can be run asynchronously.

Upvotes: 1

Omer Faruk Ozturk
Omer Faruk Ozturk

Reputation: 1872

I don't know your algorithm but first I have suggestions for some points.

  • If you want to delay, do it outside of getSumOf function for adapt Single Responsibility.
  • Use built-in reduce function to sum items in array in better and more efficient way.

You can use DispatchWorkItem to build a cancellable task. So you can remove getSumOf function and edit doSomething function like below.

let yourArray = [16,756,442,6,23]

let workItem = DispatchWorkItem {
    // Your async code goes in here
    let sum = yourArray.reduce(0, +)
    print(sum)
}

// Execute the work item after 0.3 second
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: workItem)

// You can cancel the work item if you no longer need it
workItem.cancel()

You can also look into OperationQueue for advanced use.

Upvotes: 0

matt
matt

Reputation: 535576

Using this example..., what is the way to cancel the async function?

Using that example, there is no such way. The only way to avoid printing the sum is for self to go out existence some time in the 0.3 seconds immediately after the call.

(There are ways to make a cancellable timer, but the timer you've made, assuming that it's the delay I think it is, is not cancellable.)

Upvotes: 0

Related Questions