16
Feb
2008
 

Cocoa Tutorial: NSOperation and NSOperationQueue

by Marcus Zarra

Light BulbThreading is hard in any language. And what is worse, when it goes wrong, it usually goes wrong in a very bad way. Because of this, programmers either avoid threading completely (and refer to it as the spawn of the devil) or spend a LOT of time making sure that everything is perfect.

Fortunately, Apple has made a lot of progress in OS X 10.5 Leopard. NSThread itself has received a number of very useful new methods that make threading easier. In addition, they have introduced two new objects: NSOperation and NSOperationQueue. In this Tutorial I will walk through a basic example that shows how to use these new Objects and how they make multi-threading your application nearly trivial.

You can get the example project here: Async Downloader Example Project

In this tutorial, I will demonstrate one way in which to use NSOperation and NSOperationQueue to handle tasks that are best performed on background threads. The intent of this tutorial is to demonstrate a basic use of these classes and is not intentioned to be the only way to use them.

If you are familiar with Java, or one of its variants, the NSOperation object is very similar to the java.lang.Runnable interface. Like, in Java’s Runnable interface, the NSOperation object is designed to be extended. Also like Java’s Runnable, there is a minimum of one method to override. For NSOperation that method is -(void)main. One of the easiest ways to use an NSOperation is to load it into an NSOperationQueue. As soon as the operation is loaded into the queue, the queue will kick it off and begin processing. As soon as the operation is complete the queue will release it.

NSOperation Example

In this example, I have written an NSOperation that fetches a webpage as a string, parses it into an NSXMLDocument and then passes that NSXMLDocument back to the main thread in the application before completing.

PageLoadOperation.h

1
2
3
4
5
6
7
8
9
10
11
12
#import <Cocoa/Cocoa.h>
 
 
@interface PageLoadOperation : NSOperation {
    NSURL *targetURL;
}
 
@property(retain) NSURL *targetURL;
 
- (id)initWithURL:(NSURL*)url;
 
@end

PageLoadOperation.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#import "PageLoadOperation.h"
#import "AppDelegate.h"
 
@implementation PageLoadOperation
 
@synthesize targetURL;
 
- (id)initWithURL:(NSURL*)url;
{
    if (![super init]) return nil;
    [self setTargetURL:url];
    return self;
}
 
- (void)dealloc {
    [targetURL release], targetURL = nil;
    [super dealloc];
}
 
- (void)main {
    NSString *webpageString = [[[NSString alloc] initWithContentsOfURL:[self targetURL]] autorelease];
 
    NSError *error = nil;
    NSXMLDocument *document = [[NSXMLDocument alloc] initWithXMLString:webpageString 
                                                              options:NSXMLDocumentTidyHTML 
                                                                error:&error];
    if (!document) {
        NSLog(@"%s Error loading document (%@): %@", _cmd, [[self targetURL] absoluteString], error);
        return;
    }	
 
    [[AppDelegate shared] performSelectorOnMainThread:@selector(pageLoaded:)
                                           withObject:document
                                        waitUntilDone:YES];
    [document release];
}
 
@end

As you can see, this class is very simple. It accepts a URL in the init and stores it. When the main method is called it constructs a string from the URL and then passes that string into the init of an NSXMLDocument. If there is no error with the loading of the xml document, it is then passed back to the AppDelegate, on the main thread, and the task is complete. When the main method of the NSOperation ends the queue will automatically release the object.

AppDelegate.h

1
2
3
4
5
6
7
8
9
10
#import <Cocoa/Cocoa.h>
 
@interface AppDelegate : NSObject {
	NSOperationQueue *queue;
}
 
+ (id)shared;
- (void)pageLoaded:(NSXMLDocument*)document;
 
@end

AppDelegate.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#import "AppDelegate.h"
#import "PageLoadOperation.h"
 
@implementation AppDelegate
static AppDelegate *shared;
static NSArray *urlArray;
 
- (id)init
{
    if (shared) {
        [self autorelease];
        return shared;
    }
    if (![super init]) return nil;
 
    NSMutableArray *array = [[NSMutableArray alloc] init];
    [array addObject:@"http://www.google.com"];
    [array addObject:@"http://www.apple.com"];
    [array addObject:@"http://www.yahoo.com"];
    [array addObject:@"http://www.zarrastudios.com"];
    [array addObject:@"http://www.macosxhints.com"];
    urlArray = array;
 
    queue = [[NSOperationQueue alloc] init];
    shared = self;
    return self;
}
 
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    for (NSString *urlString in urlArray) {
        NSURL *url = [NSURL URLWithString:urlString];
        PageLoadOperation *plo = [[PageLoadOperation alloc] initWithURL:url];
        [queue addOperation:plo];
        [plo release];
    }
}
 
- (void)dealloc
{
    [queue release], queue = nil;
    [super dealloc];
}
 
+ (id)shared;
{
    if (!shared) {
        [[AppDelegate alloc] init];
    }
    return shared;
}
 
- (void)pageLoaded:(NSXMLDocument*)document;
{
    NSLog(@"%s Do something with the XMLDocument: %@", _cmd, document);
}
 
@end

In this example AppDelegate, two things are occurring. First, in the init method, the NSOperationQueue is being initialized and an array of urls is being loaded. Then when the application has completed its load, the applicationDidFinishLaunching: method is called by the NSApplication instance and the AppDelegate loops over the urls, creating a task for each one and loading those tasks into the NSOperationQueue. As soon as each item is loaded into the queue the queue will kick it off by assigning it to a NSThread and the thread will then run the main method of the operation. Once the operation is complete the thread will report back to the queue and the queue will release the operation.

NSOperationQueue Concurrency

In this very simple example, it is quite difficult to load up the queue with enough objects to actually see it running them in parallel. However, if you run tasks that take more time than these, you will see that the queue will run all of the tasks at the same time. Fortunately, if you want to limit how many tasks are running in parallel you can alter the init method in the App Delegate as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (id)init
{
    if (shared) {
        [self autorelease];
        return shared;
    }
    if (![super init]) return nil;
 
    NSMutableArray *array = [[NSMutableArray alloc] init];
    [array addObject:@"http://www.google.com"];
    [array addObject:@"http://www.apple.com"];
    [array addObject:@"http://www.yahoo.com"];
    [array addObject:@"http://www.zarrastudios.com"];
    [array addObject:@"http://www.macosxhints.com"];
    urlArray = array;
    queue = [[NSOperationQueue alloc] init];
    [queue setMaxConcurrentOperationCount:2];
    shared = self;
    return self;
}

In this updated init method, the queue is throttled down to 2 operations running at the same time. The rest of the operations will wait until one of the first two is completed and then they will get an opportunity to run until the queue is empty.

Conclusion

That is the NSOperation and NSOperationQueue in its most basic form. You will note that most of the code in this example has nothing to do with the building up or using of the NSOperation or the NSOperationQueue. The actual code required to use the NSOperation is amazingly small. Yet with this small amount of code you can easily start using multiple threads in your application and provide a better experience for the user and be able to fine tune those complicated tasks.

Marcus Zarra

Marcus S. Zarra is a founding partner of MartianCraft, LLC. He has been developing Cocoa software since 2003, Java software since 1996, and has been in the industry since 1985. Currently Marcus is producing software for iOS and OS X. In addition to writing software, he assists other developers by blogging about development and supplying code samples on Cocoa Is My Girlfriend. Marcus is also the author of Core Data (2nd edition): Data Storage and Management for iOS, OS X, and iCloud and Co-Author of Core Animation: Simplified Animation Techniques for Mac and iPhone Development. You can find Marcus on Twitter, on App.net and on StackOverflow.

More Posts - Website

Follow Me:
Twitter

Comments

goron says:

Just curious… you use the new property support (for targetURL) but then don’t use the new property dot-accessor syntax.

Intentional? Also you’re not using the garbage collector. Together, these examples become rather verbose.

Marcus Zarra says:

Yes the lack of the dot accessor syntax was intentional. Why? I truly dislike it. I find it to be very jarring in the middle of all those Objective-C calls to start seeing what looks like C struct calls.

Personally, I save the dot accessor calls for when I am dealing with C structs and use message calls when dealing with Objective-C objects and messages. Keeps the code cleaner and easier to maintain.

As for the garbage collection — I am not using it any of my current projects so I do not, out of habit, use it in example projects. I am sure that will change when I am finally able to use it in a production application — or when I write a tutorial on it :)

davearter says:

In the last code block, NSOperationQueue Concurrency, you send setMaxConcurrentOperationCount: to your queue object before initialising it… Is this correct?

Marcus Zarra says:

You are correct, those two lines are reversed. Thank you for pointing that out.

jpdann says:

Hi Marcus,

Can you explain why you’ve set the urlArray as static in the AppDelegate? Does this help with threading? It seems to violate memory management conventions as it doesn’t get released. Any advantage over making it an ivar? Or is it necessary as the AppDelegate is static, too?

Jon

Marcus Zarra says:

Jon,

I did that purely out of convenience as this is a contrived example. However, there is nothing wrong with declaring a static if you expect it to stick around for the life of the application.

I could have made it an ivar with the same effect.

Marcus

Hi Marcus,

Great tutorial, saved me some time learning one of the many ways to do threading in OS X.

Some of your java terminology is a little off. Interfaces in Java are like protocols in Objective-C so the metaphor is a bit of a stretch. Nothing in the compiler is forcing you to implement -(void)main when you extend NSOperation.

NSOperation is closer to an abstract adapter in Java (java.awt.event.MouseAdapter for example) where the required methods have been created and stubbed out. You extend the class and override the methods you need.

Oh yeah, and you don’t override interface methods, you implement them ;)

M@

jpdann says:

Hi Marcus,

Thanks for the quick response. Glad to know my coding style isn’t completely off.

Jon

jpdann says:

Hi Marcus,

I noticed that your targetURL property is retained instead of copied. If you’re going to be using this retained value in a new thread then why not copy it? Can the url change from under your feet, or does the NSOperation(Queue) system watch for that?

I ask this as I have a routine that matches regexes in a large string, retaining the string would save memory, but the user can potentially edit the string in an NSTextView while the matching is taken place.

Thanks,

Jon

Marcus Zarra says:

NSURL, like NSString, is immutable therefore no copy is necessary since it is not possible for a user to alter an immutable object.

However, unlike NSString, NSURL does not have a mutable subclass so there is no concern. Therefore if you wanted to be crazy you could check to see if your incoming NSString is mutable and decide on a copy vs retain. But, I seriously down that the difference between a string copy vs. retain is going to save you a whole lot of memory.

jpdann says:

Of course, how stupid of me! I should not post until I’ve had a coffee.

Thanks Marcus

brg says:

How does performSelectorOnMainThread work ?

Does it multi-cast the call to any object that implements the pageLoaded method (thats running in the main thread)?

Or does it have magic knowledge to send it to the AppDelegate?

Marcus Zarra says:

brg,

performSelectorOnMainThread “works” just like any other message call. It is a message that is passed to an object. In this example it is passed to the static reference to the AppDelegate. However it could easily have been passed to any other NSObject. When the receiver gets the message, it then makes a subsequent call to itself on the main thread using the selector that was passed in.

CharlesAHunter says:

I am curious about the comma in your dealloc routine between the release of targetURL and the assignment to nil. I have never seen comma syntax in this situation before and am wondering whether I’m missing something? (Kernighan & Ritchie, 1978, Appendix A.7.15) gives an example of comma syntax in an expression of “f(a,(t=3,t+2),c)” saying that f() is being pass three arguments: a, 5 and c. However, that is not what appears to be going on in this dealloc routine. Is it just a typo that happens to compile or is it more significant?

Marcus Zarra says:

The comma is used to chain commands together on one line. That is all that it does and it is a part of the C language. I use it when I do releases so that I do not forget to set the pointer to nil. This is a bit of protective coding to avoid accidently send a message to a released object.

bguest says:

Very succinct tutorial, just what I needed, thank you.

Eaglelouk says:

Seems that the NSXMLDocument is leaked. maybe you should use [document autorelease] when using performSelectorOnMainThread.

Nice tuto anyway!

Marcus Zarra says:

The document is not leaked. It is released after the main thread has completed. Line 35 of PageLoadOperation.m to be specific.

octy says:

Hi Marcus,

Great stuff, thanks for this post.

I’m wondering about one thing though… in the dealloc method (AppDelegate.m:41): why do you set the queue pointer to nil? Isn’t the pointer going away when dealloc is called? I’m not nitpicking, just trying to understand a little bit more about object lifecycle in Objective-C.

Thanks!

Marcus Zarra says:

It is set to nil for a couple of reasons:

  1. If something after that call tries to access the ivar then it will be a no op because it is nil rather than an exception because it is a reference to a released object.

  2. In GC land, -release is a no op and setting an object to nil is a further guarantee that the GC will behave and decrease the reference count.

In either case it is extra protection for your code and a good habit to get into.

karthik says:

nice tutorial.Thanks

chic says:

thank you for this tutorial,

I have 2 questions after it: . is there big performance difference using nsoperation or nsthread . is it possible to cancel the operation, for example if user go back

Marcus Zarra says:

Performance difference? No. Safety yes.

Yes an operation can be cancelled per the API.

abhisek says:

Marcus, great tutorial. one questn. Why can’t we define variables as static in @interface, likewise you defined static in @implementation. Also as I am new to cocoa I’d like to knw if its sure that init in appdelegate will be called and will finish its work before applicationDidFinishLaunching is called??

Regards, Abhisek

Marcus Zarra says:

static variables belong in the implementation file, not in the header. -init is always called before anything else on the instance of the class. It initializes the instance after its memory has been allocated via -alloc.

abhisek says:

If I’m creating a big application and I need to declare an NSOperationQueue which can be accessible everywhere by all the Operations(Threads), where shall I do it. In AppDelegate(should it be static?) ? or in main.m ?

Marcus Zarra says:

I highly suggest posting questions like this on stackoverflow.com. You will get far more detailed answers.