NSOperation was introduced with Leopard as an attempt by Apple to simplify the task of writing multi-threaded applications in Objective-C. This will become increasingly important as we move away from faster processors to more cores and you can be sure that NSOperation is only the start of what Apple has in store for us in this area. In this article I discuss a fun little use of NSOperation and, hey, there is a little Core Animation thrown in to boot.
When I was at school I read James Gleick's book Chaos and decided I would write an application on my Dad's PC to draw a Mandelbrot set. I have no idea now what it was written in, it might even have been QuickBasic, but what I do remember is how slow it was. I would type in the co-ordinates and leave it for an hour or so before a small mono-chrome Mandelbrot image the size of a postcard appeared. I was quite pleased with it.
Fast forward by more years than I care to mention and I am the proud new owner of a G5 iMac, looking for a little project with which to get to grips with Xcode and Objective-C. So I decided to write a Mandelbrot screensaver. The results were a lot faster and a lot more colourful than my previous attempt, and I must have spent hours sitting watching the strange and beautiful patterns it produced. But there was a definite pause of between 2 to 5 seconds while the calculations were carried out.
So finally I've re-written it once more for this blog post using NSOperation. In fact we thought it was actually good enough to be released properly as a free screensaver. A cut-down version of the code for this project can be found here (cut-down simply because the actual project contains Sparkle updates and the like) or if you just want the screensaver itself you can get that from the downloads section of our web-site. I won't go into detail about writing the screensaver itself, but if you are interested Brian Christensen's excellent two part article is as good now as it was when I first read it all that time ago.
As anyone who has ever written any multi-threaded application will know, things start to get complicated when different threads need to interact. Traditionally this problem was handled using synchronisation mechanisms such as signalling waiting threads. This is fine until two threads manage to get into a situation where each is waiting on a signal from the other - which usually won't happen until the application is deployed on a client's machine.
Apple have helped us mortal developers get around this problem by introducing the concept of an operation queue. Independent packages of work, operations, are added to the queue and synchronisation is achieved by adding dependancies between these operations; if operation B is dependant on operation A then we are guaranteed that operation A has run to completion by the time operation B starts. Without dependancies, other operations on the queue will execute whenever they get a chance.
This model translates into three Cocoa classes: NSOperationQueue, NSOperation and NSInvocationOperation. NSOperationQueue is responsible for handling the queuing of operations (hence the name - I really couldn't think of a better way to say that). Instances of either NSOperation or NSInvocationOperation or both are placed into the queue using the addOperation: message.NSInvocationOperation provides the simplest method of adding an operation as no sub-classing is required. Instead, instances of the class are initialised using the initWithTarget:selector:object: method. For example the code:
NSInvocationOperation *pNotifyOperation = ;will set up an operation that will call the
notifyImageComplete method on self to perform its task.NSOperation on the other hand is designed to be subclassed with an overwritten main method provided to carry out the operation's task.So let's see how all this fits into the example of the Mandelbrot screensaver.
To display the Mandelbrot image we have three CALayers in the
ScreenSaverView derived class: currentLayer, nextLayer and renderingLayer. Calculations are performed in the background and the results rendered to renderingLayer. When the application receives notification that it should refresh its view (via the animateOneFrame method) it checks to see whether this rendering has finished and replaces currentLayer with nextLayer, nextLayer with renderingLayer and renderingLayer with currentLayer. This lets us animate cross-fades between currentLayer and nextLayer without the risk of the rendering changing one of these layers half way through.Rendering to the CALayers takes place in the
MandelbrotImageGenerator class, which is also responsible for handling interactions with the NSOperationQueue and its operations.In our project we have a
NSOperation. The purpose of this class, as illustrated in the figure below, is to render a given portion of the Mandelbrot set into an image buffer. We derive a class (rather than using NSInvocationOperation) as we need a lot of additional information to carry out our task; the buffer to draw into, the buffer size, the visible portion to be drawn, etc.
We need to be notified once all the drawing operations have completed and we achieve this using an
NSInvocationOperation which has a dependancy on each of the drawing operations. Here is the relevant code (cut down to show only those bits of importance to the discussion).// Create the final operation to send the notification once everything is complete
NSInvocationOperation *pNotifyOperation = ;
...
for( bottom = 0; bottom < height; bottom += dy )
// Add the notification operation
;
An important point to note is that the final notification operation is not added to the queue until all of the drawing operations have been added. If the notification operation was added to the queue first it could have executed in a separate thread before we've had a chance to add any of the drawing operations to the queue.
The
notifyImageComplete method simply passes on the notification to the main thread. Drawing the image into its CALayer is safer on the main thread as we do not want to risk this memory being released half way through.-(void) notifyImageComplete
During rendering of the image we can periodically check that the operation has not been cancelled by checking the
isCancelled property of self, which may be the case if the application is terminating and we are in the process of clearing up. Operations may be cancelled directly. However, in our case we simply want to cancel all currently running operations and wait until they have finished. This takes place in the dealloc method of the MandelbrotImageGenerator class:;
;
So that's it. Although we still need to think carefully about how concurrent operations access the same data, I hope you can see that much of the complexity involved in synchronising this has been removed.
0 Comments:
Post a Comment