6

I have this code to add a NSOperation instance to a queue

let operation = NSBlockOperation()
operation.addExecutionBlock({
    self.asyncMethod() { (result, error) in
        if operation.cancelled {
            return
        }

        // etc
    }
})
operationQueue.addOperation(operation)

When user leaves the view that triggered this above code I cancel operation doing

operationQueue.cancelAllOperations()

When testing cancelation, I'm 100% sure cancel is executing before async method returns so I expect operation.cancelled to be true. Unfortunately this is not happening and I'm not able to realize why

I'm executing cancellation on viewWillDisappear

EDIT

asyncMethod contains a network operation that runs in a different thread. That's why the callback is there: to handle network operation returns. The network operation is performed deep into the class hierarchy but I want to handle NSOperations at root level.

StackOverflower
  • 5,463
  • 13
  • 58
  • 89
  • you have to be sure, that operation.cancelled is true before the test, not just before async method returns ... – user3441734 Oct 30 '15 at 10:08
  • @user3441734: what do you mean with "before the test"? It's impossible the operation to be cancelled before async method starts because there is no logic. If it's cancelled before, it simply won't run at all. – StackOverflower Oct 30 '15 at 11:49
  • " I'm 100% sure cancel is executing before async method returns so I expect operation.cancelled to be true". the async method will return, even though operation.cancelled is false. it just not return 'early'. – user3441734 Oct 30 '15 at 12:09
  • @user3441734: sorry but I don't get your point. I'm not expecting async method to return early. I just want to avoid executing callback logic trough `operation.cancelled` check. – StackOverflower Oct 30 '15 at 13:38
  • avoid execution? it is your responsibility .... for that purpose you test if operation.cancelled is true or false, not? there is no miracle behind, if operation.cancelled is true, dont't continue the job (you can test it again and again ... everywhere in you code, and return from the job (finish it) 'early' – user3441734 Oct 30 '15 at 15:43
  • You're asking the wrong question. There's no guarantee that the check you're doing within the execution block is always performed before the `cancelled` variable is set to `YES`. Using breakpoints and debugger doesn't help to understand what happens there. You are a blameless victim of race conditions :) – HepaKKes Nov 08 '15 at 13:25

5 Answers5

6

Calling the cancel method of this object sets the value of this property to YES. Once canceled, an operation must move to the finished state.

Canceling an operation does not actively stop the receiver’s code from executing. An operation object is responsible for calling this method periodically and stopping itself if the method returns YES.

You should always check the value of this property before doing any work towards accomplishing the operation’s task, which typically means checking it at the beginning of your custom main method. It is possible for an operation to be cancelled before it begins executing or at any time while it is executing. Therefore, checking the value at the beginning of your main method (and periodically throughout that method) lets you exit as quickly as possible when an operation is cancelled.

import Foundation

let operation1 = NSBlockOperation()
let operation2 = NSBlockOperation()
let queue = NSOperationQueue()
operation1.addExecutionBlock { () -> Void in
    repeat {
        usleep(10000)
        print(".", terminator: "")
    } while !operation1.cancelled
}
operation2.addExecutionBlock { () -> Void in
    repeat {
        usleep(15000)
        print("-", terminator: "")
    } while !operation2.cancelled
}
queue.addOperation(operation1)
queue.addOperation(operation2)
sleep(1)
queue.cancelAllOperations()

try this simple example in playground.

if it is really important to run another asynchronous code, try this

operation.addExecutionBlock({
if operation.cancelled {
            return
        }    
self.asyncMethod() { (result, error) in


        // etc
    }
})
user3441734
  • 16,722
  • 2
  • 40
  • 59
  • 1
    Please read my question correctly. I'm do checking `cancelled` value. My problem is that is always `false`, even when I cancelled operation – StackOverflower Oct 31 '15 at 20:44
  • if your self.asyncMethod() is really something asynchronous, it returns immediately and the execution block also exits immediately. i updated my answer with very basic example how to use NSOperation and NSOperationQueue – user3441734 Oct 31 '15 at 21:22
  • I appreciate you putting all this together but I don't see any difference between my code and yours. I've put a debugger on line after operation cancellation and on async method callback, I validated that cancelation executes before `if operation.cancelled ` but cancelled remains in `false` – StackOverflower Nov 02 '15 at 11:34
  • 1
    why are you executing another asynchronous code inside operation block? on witch queue this code is running? in your closure, the value reflect the captured operation.cancelled value and will never changed ... if it is necessary to run that code 'as is' you have to check operation.cancelled value before you dispatch the job to another queue – user3441734 Nov 02 '15 at 12:51
  • async method access to network resources trough a third party component and I don't have control over that. I need to check operation status inside the callback or look for a different approach. It doesn't make sense on my scenario to check status before calling async method. Thanks – StackOverflower Nov 02 '15 at 18:23
  • in your example, it was on the very beginning ... what is the difference? if you will check cancelled before dispatch the job, the job will not be dispached if your operation will be cancelled. what behavior do you need? – user3441734 Nov 02 '15 at 19:32
2

it's because you doing work wrong. You cancel operation after it executed. Check this code, block executed in one background thread. Before execution start – operation cancel, remove first block from queue.

Swift 4

let operationQueue = OperationQueue()
operationQueue.qualityOfService = .background

let ob1 = BlockOperation {
    print("ExecutionBlock 1. Executed!")
}

let ob2 = BlockOperation {
    print("ExecutionBlock 2. Executed!")
}

operationQueue.addOperation(ob1)
operationQueue.addOperation(ob2)

ob1.cancel()

// ExecutionBlock 2. Executed!

Swift 2

let operationQueue = NSOperationQueue()
operationQueue.qualityOfService = .Background

let ob1 = NSBlockOperation()
ob1.addExecutionBlock {
    print("ExecutionBlock 1. Executed!")
}

let ob2 = NSBlockOperation()
ob2.addExecutionBlock {
    print("ExecutionBlock 2. Executed!")
}

operationQueue.addOperation(ob1)
operationQueue.addOperation(ob2)

ob1.cancel()

// ExecutionBlock 2. Executed!
dimpiax
  • 12,093
  • 5
  • 62
  • 45
1

The Operation does not wait for your asyncMethod to be finished. Therefore, it immediately returns if you add it to the Queue. And this is because you wrap your async network operation in an async NSOperation.

NSOperation is designed to give a more advanced async handling instead for just calling performSelectorInBackground. This means that NSOperation is used to bring complex and long running operations in background and not block the main thread. A good article of a typically used NSOperation can be found here:

http://www.raywenderlich.com/19788/how-to-use-nsoperations-and-nsoperationqueues

For your particular use case, it does not make sense to use an NSOperation here, instead you should just cancel your running network request.

charlyatwork
  • 1,187
  • 8
  • 13
1

It does not make sense to put an asynchronous function into a block with NSBlockOperation. What you probably want is a proper subclass of NSOperation as a concurrent operation which executes an asynchronous work load. Subclassing an NSOperation correctly is however not that easy as it should.

You may take a look here reusable subclass for NSOperation for an example implementation.

Community
  • 1
  • 1
CouchDeveloper
  • 18,174
  • 3
  • 45
  • 67
0

I am not 100% sure what you are looking for, but maybe what you need is to pass the operation, as parameter, into the asyncMethod() and test for cancelled state in there?

operation.addExecutionBlock({
  asyncMethod(operation) { (result, error) in
  // Result code
  }
})
operationQueue.addOperation(operation)

func asyncMethod(operation: NSBlockOperation, fun: ((Any, Any)->Void)) {
  // Do stuff...
  if operation.cancelled {
    // Do something...
    return // <- Or whatever makes senes
  }
}
Mikael Hellman
  • 2,664
  • 14
  • 22