- Dataflow Programming / Concurrent Operations
- Operation Types / Adding Dependencies
- Cancelling Operations / Returning to Synchronous Behavior
Cancelling Operations
You're supposed to be able to cancel operations. Sometimes this functionality isn't needed, and you can ignore it, but often being able to cancel is necessary. It's quite useful to use NSOperations to manage long-running activities that happen in response to use actions. Users are generally buggy, so these actions can often be contradictory. For example, there's no point in continuing to generate data if the user has already closed the window that would display the result.
NSOperations have a cancelled property that can be set explicitly on a single operation by you, or by NSOperationQueue in response to a -cancelAllOperations message. Operations are expected to check this property in their -main method. For operations that will complete quickly, it isn't worth implementing this check. If an operation will take a long time (several seconds or longer), it should periodically do something like this:
if ([self isCancelled]) { return; }
Of course, the return statement may need to clean up any temporary state that the operation is managing.
The decision as to whether to support cancellation is not trivial. The documentation says that every operation should support cancellation, which is quite entertaining, as the NSInvocationOperation-the only concrete operation class that Apple shipped when that part of the documentation was written-didn't.
If you're using NSOperation efficiently, each of your operations will take a relatively small amount of time, but you'll add a lot of operations to the queue at once. In this case, you don't gain very much from properly supporting cancellation. In other cases, it's very important. For example, you might use an operation queue to get a lot of data from different network sources. These may take seconds to respond and minutes to deliver the data. If you don't check for cancellation periodically, you'll continue to fetch data for a long time after the user has stopped caring.
Returning to Synchronous Behavior
Quite often, you want to use an operation queue to start a load of jobs, and later rendezvous with the result. In a simple threading environment, you'd use something like pthread_join() to make the first thread wait for the spawned thread to finish. There are several ways of doing this with NSOperation. The simplest is to send a -waitUntilFinished message to the last operation in a group. This will block until the operation enters its finished state.
If you fired off a whole set of operations at once, however, you may want to wait for all of them to finish. You can do this by sending a -waitUntilAllOperationsAreFinished message to the operation queue. When doing this, be careful to use a queue that you created for this set of operations, rather than a shared queue, or you may end up waiting a very long time.
These methods are both fine for command-line applications that want to start some tasks, run them in parallel, and then exit when they're done. They're less useful with graphical applications, because users have an unfortunate habit of objecting if the app that they're using freezes while working in the background.
In most AppKit or UIKit applications, your main thread will be using an NSRunLoop instance and will be invoking various methods in response to events. You can take advantage of this fact in conjunction with an operation's completion block to deliver a notification in the main thread once some background work is done. Any operation can have an associated completion block. This block is invoked after the operation finishes running.
To get a notification in the main thread, we can take advantage of some convenient methods in NSObject:
notifiedObject = someObject; [operation setCompletionBlock: ^(void) { [notifiedObject performSelectorOnMainThread: @selector(operationCompleted:) withObject: operation waitUntilDone: NO]; }]
This technique causes the notified object to receive an -operationCompleted: message with the operation as the argument, in the main thread, once the operation has run. This is a very convenient way of starting a load of background tasks and then acting on the result.
You can use the same mechanism to update progress indicators. If you're starting a dozen operations in the background, for instance, you can add a completion block to each operation, which will update the value of the progress indicator in the main thread once it's finished.
I think this usage shows the real power of NSOperation: You can use it to make the processor-limited parts of your code multithreaded, but still have the rest as simple single-threaded code.