Question

I am unable to find good documentation about how to subclass NSOperation to be concurrent and also to support cancellation. I read the Apple docs, but I am unable to find an "official" example.

Here is my source code :

@synthesize isExecuting = _isExecuting;
@synthesize isFinished = _isFinished;
@synthesize isCancelled = _isCancelled;

- (BOOL)isConcurrent
{
    return YES;
}

- (void)start
{
/* WHY SHOULD I PUT THIS ?
    if (![NSThread isMainThread])
    {
        [self performSelectorOnMainThread:@selector(start) withObject:nil waitUntilDone:NO];
        return;
    }
*/

    [self willChangeValueForKey:@"isExecuting"];
    _isExecuting = YES;
    [self didChangeValueForKey:@"isExecuting"];


    if (_isCancelled == YES)
    {
        NSLog(@"** OPERATION CANCELED **");
    }
    else
    {
        NSLog(@"Operation started.");
        sleep(1);
        [self finish];
    }
}

- (void)finish
{
    NSLog(@"operationfinished.");

    [self willChangeValueForKey:@"isExecuting"];
    [self willChangeValueForKey:@"isFinished"];

    _isExecuting = NO;
    _isFinished = YES;

    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];

    if (_isCancelled == YES)
    {
        NSLog(@"** OPERATION CANCELED **");
    }
}

In the example I found, I don't understand why performSelectorOnMainThread: is used. It would prevent my operation from running concurrently.

Also, when I comment out that line, I get my operations running concurrently. However, the isCancelled flag is not modified, even though I have called cancelAllOperations.

Was it helpful?

Solution

Okay, so as I understand it, you have two questions:

  1. Do you need the performSelectorOnMainThread: segment that appears in comments in your code? What does that code do?

  2. Why is the _isCancelled flag is not modified when you call cancelAllOperations on the NSOperationQueue that contains this operation?

Let's deal with these in order. I'm going to assume that your subclass of NSOperation is called MyOperation, just for ease of explanation. I'll explain what you're misunderstanding and then give a corrected example.

1. Running NSOperations concurrently

Most of the time, you'll use NSOperations with an NSOperationQueue, and from your code, it sounds like that's what you're doing. In that case, your MyOperation will always be run on a background thread, regardless of what the -(BOOL)isConcurrent method returns, since NSOperationQueues are explicitly designed to run operations in the background.

As such, you generally do not need to override the -[NSOperation start] method, since by default it simply invokes the -main method. That is the method you should be overriding. The default -start method already handles setting isExecuting and isFinished for you at the appropriate times.

So if you want an NSOperation to run in the background, simply override the -main method and put it on an NSOperationQueue.

The performSelectorOnMainThread: in your code would cause every instance of MyOperation to always perform its task on the main thread. Since only one piece of code can be running on a thread at a time, this means that no other MyOperations could be running. The whole purpose of NSOperation and NSOperationQueue is to do something in the background.

The only time you want to force things onto the main thread is when you're updating the user interface. If you need to update the UI when your MyOperation finishes, that is when you should use performSelectorOnMainThread:. I'll show how to do that in my example below.

2. Cancelling an NSOperation

-[NSOperationQueue cancelAllOperations] calls the -[NSOperation cancel] method, which causes subsequent calls to -[NSOperation isCancelled] to return YES. However, you have done two things to make this ineffective.

  1. You are using @synthesize isCancelled to override NSOperation's -isCancelled method. There is no reason to do this. NSOperation already implements -isCancelled in a perfectly acceptable manner.

  2. You are checking your own _isCancelled instance variable to determine whether the operation has been cancelled. NSOperation guarantees that [self isCancelled] will return YES if the operation has been cancelled. It does not guarantee that your custom setter method will be called, nor that your own instance variable is up to date. You should be checking [self isCancelled]

What you should be doing

The header:

// MyOperation.h
@interface MyOperation : NSOperation {
}
@end

And the implementation:

// MyOperation.m
@implementation MyOperation

- (void)main {
    if ([self isCancelled]) {
        NSLog(@"** operation cancelled **");
    }

    // Do some work here
    NSLog(@"Working... working....")

    if ([self isCancelled]) {
        NSLog(@"** operation cancelled **");
    }
    // Do any clean-up work here...

    // If you need to update some UI when the operation is complete, do this:
    [self performSelectorOnMainThread:@selector(updateButton) withObject:nil waitUntilDone:NO];

    NSLog(@"Operation finished");
}

- (void)updateButton {
    // Update the button here
}
@end

Note that you do not need to do anything with isExecuting, isCancelled, or isFinished. Those are all handled automatically for you. Simply override the -main method. It's that easy.

(A note: technically, this is not a "concurrent" NSOperation, in the sense that -[MyOperation isConcurrent] would return NO as implemented above. However, it will be run on a background thread. The isConcurrent method really should be named -willCreateOwnThread, as that is a more accurate description of the method's intention.)

OTHER TIPS

The excellent answer of @BJHomer deserves an update.

Concurrent operations should override the start method instead of main.

As stated in the Apple Documentation:

If you are creating a concurrent operation, you need to override the following methods and properties at a minimum:

  • start
  • asynchronous
  • executing
  • finished

A proper implementation also requires to override cancel as well. Making a subclass thread-safe and getting the required semantics right is also quite tricky.

Thus, I've put a complete and working subclass as a proposal implemented in Swift in Code Review. Comments and suggestion are welcome.

This class can be easily used as a base class for your custom operation class.

I know this is an old question, but I've been investigating this lately and encountered the same examples and had the same doubts.

If all your work can be run synchronously inside the main method, you don't need a concurrent operation , neither overriding start, just do your work and return from main when done.

However, if your workload is asynchronous by nature - i.e loading a NSURLConnection, you must subclass start. When your start method returns, the operation isn't finished yet. It will only be considered finished by the NSOperationQueue when you manually send KVO notifications to the isFinished and isExecuting flags (for instance, once the async URL loading finishes or fails).

Lastly, one might want to dispatch start to the main thread when the async workload you want to start require a run-loop listening on the main thread. As the work itself is asynchronous, it won't limit your concurrency, but starting the work in a worker thread might not have a proper runloop ready.

Take a look at ASIHTTPRequest. It is an HTTP wrapper class built on top of NSOperation as a subclass and seems to implement these. Note that as of mid-2011, the developer recommends not using ASI for new projects.

Regarding define "cancelled" property (or define "_cancelled" iVAR) inside NSOperation subclass, normally that is NOT necessary. Simply because when USER triggers the cancellation, custom code should always notify KVO observers that your operation is now finished with its work. In other words, isCancelled => isFinished.

Particularly, when NSOperation object is dependent on the completion of other operation objects, it monitors the isFinished key path for those objects. Failing to generate a finish notification (in case of cancellation happens) can therefore prevent the execution of other operations in your application.


BTW, @BJ Homer's answer: "The isConcurrent method really should be named -willCreateOwnThread" makes A LOT sense!

Because if you do NOT override start-method,simply manually call NSOperation-Object's default-start-method, calling-thread itself is, by default, synchronous; so, NSOperation-Object is only a non-concurrent operation.

However, if you DO override start-method, inside start-method implementation, custom-code should spawn a separate thread…etc,then you successfully break the restriction of "calling-thread default being synchronous", therefore making NSOperation-Object becoming a concurrent-operation, it can run asynchronously afterwards.

This blog post:

http://www.dribin.org/dave/blog/archives/2009/09/13/snowy_concurrent_operations/

explains why you might need:

if (![NSThread isMainThread])
{
    [self performSelectorOnMainThread:@selector(start) withObject:nil waitUntilDone:NO];
    return;
}

in your start method.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top