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
.
Overview. An operation queue invokes its queued NSOperation objects based on their priority and readiness. After you add an operation to a queue, it remains in the queue until the operation finishes its task. You can't directly remove an operation from a queue after you add it. Note.
An abstract class that represents the code and data associated with a single task.
Overview. The NSBlockOperation class is a concrete subclass of NSOperation that manages the concurrent execution of one or more blocks. You can use this object to execute several blocks at once without having to create separate operation objects for each.
Okay, so as I understand it, you have two questions:
Do you need the performSelectorOnMainThread:
segment that appears in comments in your code? What does that code do?
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.
Most of the time, you'll use NSOperation
s 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 NSOperationQueue
s 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 MyOperation
s 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.
-[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.
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.
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]
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.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With