Operations and Operation Queues
In Chapter 11, you learned about operation objects, instances of the NSOperation class (and its subclasses) that encapsulate the code and data for a single task. As an operation object encapsulates a single unit of work, it is an ideal vehicle for implementing concurrent programming. The Foundation Framework includes the following three operation classes:
- NSOperation: The base (abstract) class for defining operation objects. For nonconcurrent tasks, a concrete subclass typically only needs to override themain method. For concurrent tasks, you must override at a minimum the methods start, isConcurrent, isExecuting, and isFinished.
- NSBlockOperation: A concrete subclass of NSOperation that is used to execute one or more block objects concurrently. An NSBlockOperationobject is considered finished only when all of its block objects have finished execution.
- NSInvocationOperation: A concrete subclass of NSOperation that is used to create an operation object based on an object and selector that you specify.
The following statement creates an NSBlockOperation instance named greetingOp.
NSBlockOperation* greetingOp = [NSBlockOperation blockOperationWithBlock: ^{
NSLog(@"Hello, World!");
}];
You can also add additional blocks to an NSBlockOperation instance using theaddExecutionBlock: method. The following statement adds a block to theNSBlockOperation instance greetingOp.
[greetingOp addExecutionBlock: ^{
NSLog(@"Goodbye");
}];
An NSInvocationOperation can be created and initialized using either an NSInvocationobject or a selector and receiver object. The following statement creates anNSInvocationOperation with the selector hello and a receiver object named greetingObj.
NSInvocationOperation invokeOp = [[NSInvocationOperation alloc]
initWithTarget:greetingObj selector:@selector(hello)];
You can also implement custom operation classes. A custom operation class subclassesNSOperation and must implement, at a minimum, the main method to perform the desired task. Optionally, it can also provide the following functionality:
- Custom initialization methods
- Custom helper methods (invoked via the main method)
- Accessor methods for setting data values and accessing the results of the operation
- Methods that conform the class to the NSCoding protocol (to support archiving the object)
Operation objects support a variety of features that facilitate concurrent programming, several of which are
- Establishing dependencies between operation objects, thereby enabling you to control their order of execution.
- Creating a completion block that is executed after an operation’s main task has finished.
- Retrieving an operation’s execution state.
- Prioritizing operations in an operation queue.
- Cancelling operations.
An operation object is executed by invoking its start method. The default implementation of this method executes the operation’s task (implemented by its main method) synchronously. Hence, you may be wondering how operation objects support concurrent programming. Well, operation objects are typically executed by adding them to operation queues, which provide built-in support for executing operations concurrently. Specifically, operation queues provide threads for executing operations.
An operation queue is a mechanism that provides the capability to execute operations concurrently. The Foundation Framework NSOperationQueue class is an Objective-C implementation of an operation queue. An operation can be added to an NSOperationQueueinstance as a block object or an instance of a subclass of NSOperation. An operation queue manages the execution of operations. Thus it includes methods to manage operations in the queue, manage the number of running operations, suspend operations, and retrieve specific queues. Listing 17-16 creates and initializes an NSOperationQueue instance and then uses itsaddOperationWithBlock: method to submit a block object to the queue.
Listing 17-16. Adding a Block Object to an Operation Queue
NSOperationQueue *queue = [NSOperationQueue new];
[queue addOperationWithBlock: ^{
NSLog(@"Hello, World!");
}];
[queue waitUntilAllOperationsAreFinished];
Once an operation is added to a queue, it remains in the queue until it is explicitly cancelled or finishes executing its task. You can cancel an (NSOperation) object added to an operation queue by invoking its cancel method or by invoking the cancelAllOperations method on the queue.
The execution order of operations within a queue is a function of the priority level of each operation and the interoperation object dependencies. The current implementation ofNSOperationQueue uses Grand Central Dispatch to initiate execution of their operations. As a result, each operation in the queue is executed in a separate thread.
Operation objects and operation queues provide an object-oriented mechanism for performing asynchronous, concurrent programming. They eliminate the need for low-level thread management, and simplify synchronization and coordination of execution for multiple interdependent tasks. Because they utilize system services that can scale dynamically in response to resource availability and utilization, they ensure that tasks are executed as quickly and as efficiently as possible.
Executing Operation Objects Manually
Although operation objects are typically executed using operation queues, it is possible to start an operation object manually (i.e., not add it to a queue). To do this, you must code the operation as a concurrent operation in order to have it execute it asynchronously. This is accomplished by performing the following steps:
- Override the start method. This method should be updated to execute the operation asynchronously, typically by invoking its main method in a new thread.
- Override the main method (optional). In this method, you implement the task associated with the operation. If preferred, you can skip this method and implement the task in the start method, but overriding the main method provides a cleaner design that is consistent with the intent.
- Configure and manage the operation’s execution environment. Concurrent operations must set up their environment and report its status to clients. Specifically, the isExecuting, isFinished, and isConcurrent methods must return appropriate values relative to the operation’s state, and these methods must be thread-safe. These methods must also generate the appropriate key-value (KVO) observer notifications when these values change.
Note Key-value observing is an Objective-C language mechanism that enables objects to be notified of changes to specified properties of other objects. Chapter 18 examines key-value programming in depth.
To highlight the differences between a nonconcurrent operation object (typically executed via an operation queue) versus a concurrent operation object, let’s look at some code. Listing 17-17 illustrates the implementation of a custom, nonconcurrent operation class namedGreetingOperation.
Listing 17-17. Minimal Implementation of a Custom, Nonconcurrent Operation Class
@implementation GreetingOperation
- (void)main
{
@autoreleasepool
{
@try
{
if (![self isCancelled])
{
// Insert code to implement the task below
NSLog(@"Hello, World!");
[NSThread sleepForTimeInterval:3.0];
NSLog(@"Goodbye, World!");
}
}
@catch (NSException *ex) {}
}
}
@end
As shown in Listing 17-17, the code to perform the task is implemented in the main method. Note that this method includes an autorelease pool and a try-catch block. The autorelease pool prevents memory leaks from the associated thread, while the try-catch block is required to prevent any exceptions from leaving the scope of this thread. The main method also checks if the operation is cancelled in order to quickly terminate its execution if it is no longer needed. To invoke this operation asynchronously, you can add it to an operation queue, as shown inListing 17-18.
Listing 17-18. Executing a Custom Operation in an Operation Queue
NSOperationQueue *queue = [NSOperationQueue new];
GreetingOperation *greetingOp = [GreetingOperation new];
[greetingOp setThreadPriority:0.5];
[queue addOperation:greetingOp];
[queue waitUntilAllOperationsAreFinished];
This demonstrates the steps required to implement a nonconcurrent operation and submit it to an operation queue for execution. In the next section, you will implement a concurrent operation to understand the differences between the two options.
Implementing Concurrent Operations
Now you will create a program that implements a custom, concurrent operation. It will provide the same functionality as the program shown in Listing 17-14 and enable you to compare the differences between the two implementations. In Xcode, create a new project by selectingNew Project . . . from the Xcode File menu. In the New Project Assistant pane, create a command-line application. In the Project Options window, specify GreetingOperation for the Product Name, choose Foundation for the Project Type, and select ARC memory management by checking the Use Automatic Reference Counting check box. Specify the location in your file system where you want the project to be created (if necessary, selectNew Folder and enter the name and location for the folder), uncheck the Source Controlcheck box, and then click the Create button.
Next you will create the custom operation class. Select New File . . . from the Xcode File menu, select the Objective-C class template, and name the class GreetingOperation. Make the class a subclass of NSOperation, select the GreetingOperation folder for the files location and the GreetingOperation project as the target, and then click the Create button. In the Xcode project navigator pane, select the GreetingOperation.m file and update the class implementation, as shown in Listing 17-19.
Listing 17-19. GreetingOperation Implementation
#import "GreetingOperation.h"
@implementation GreetingOperation
{
BOOL finished;
BOOL executing;
}
- (id)init
{
if ((self = [super init]))
{
executing = NO;
finished = NO;
}
return self;
}
- (void)start
{
// If cancelled just return
if ([self isCancelled])
{
[self willChangeValueForKey:@"isFinished"];
finished = YES;
[self didChangeValueForKey:@"isFinished"];
return;
}
// Now execute in main method a separate thread
[self willChangeValueForKey:@"isExecuting"];
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
executing = YES;
[self didChangeValueForKey:@"isExecuting"];
}
- (void)main
{
@autoreleasepool
{
@try
{
if (![self isCancelled])
{
NSLog(@"Hello, World!");
// Pause to simulate processing being performed by task
[NSThread sleepForTimeInterval:3.0];
NSLog(@"Goodbye, World!");
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
executing = NO;
finished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
}
@catch (NSException *ex) {}
}
}
- (BOOL)isConcurrent
{
return YES;
}
- (BOOL)isExecuting
{
return executing;
}
- (BOOL)isFinished
{
return finished;
}
@end
Compared to the nonconcurrent GreetingOperation implementation in Listing 17-17, there are a number of changes. First, observe the declaration of two private variables.
{
BOOL finished;
BOOL executing;
}
These variables are used to set and return the appropriate values for the isFinished andisExecuting methods. Recall that these methods (along with the isConcurrent method) must be overridden for concurrent operations. Now let’s look at the implementation of thestart method. This was not implemented for the nonconcurrent version of theGreetingOperation class. First, it checks to see whether or not the operation has been cancelled; if it has, it simply sets the finished variable appropriately for KVO notifications and returns.
if ([self isCancelled])
{
[self willChangeValueForKey:@"isFinished"];
finished = YES;
[self didChangeValueForKey:@"isFinished"];
return;
}
If not cancelled, the code sets up a new thread and uses it to invoke the main method that implements the associated task, while also performing the appropriate KVO notifications.
[self willChangeValueForKey:@"isExecuting"];
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
executing = YES;
[self didChangeValueForKey:@"isExecuting"];
Now let’s examine the class’s main method. This method has identical functionality to that of the main method in Listing 17-14, with the addition of KVO notifications to indicate the current operation state. Also note the statement that pauses the thread for three seconds to simulate task processing.
[NSThread sleepForTimeInterval:3.0];
Finally, the remaining methods implement the required isExecuting, isFinished, andisConcurrent methods, returning the appropriate value in each case.
OK, now that you have finished implementing the custom operation class, let’s move on to the main() function. In the Xcode project navigator, select the main.m file and update themain() function, as shown in Listing 17-20.
Listing 17-20. GreetingOperation main( ) Function
#import <Foundation/Foundation.h>
#import "GreetingOperation.h"
int main(int argc, const char * argv[])
{
@autoreleasepool
{
GreetingOperation *greetingOp = [GreetingOperation new];
[greetingOp start];
while (![greetingOp isFinished])
;
}
return 0;
}
The main() function begins by creating a GreetingOperation object. It then executes the operation by invoking its start method. Finally, a conditional expression using the object’sisFinished method is used to end execution of the program when the concurrent operation is finished.
When you compile and run the program, you should observe the messages in the output pane shown in Figure 17-6.
Figure 17-6. GreetingOperation program output
In the output pane, the task displays the initial greeting followed by a delay of approximately 3 seconds, and then the final message. The program exits when the thread finishes execution per the conditional expression. As you learned from this example, a considerable amount of additional functionality must be coded to correctly implement a custom concurrent operation class. Hence, you should only do this if you need to have an operation object execute asynchronously without adding it to a queue.