Nifty Box News

Wednesday, December 20, 2006

Worker Thread - NSInvocation Part 2

NSInvocation is not only nice to post delayed multi-argument messages, you can also use it for constructing a lightweight worker thread implementation.

Sometimes it happens that your app has to do lot of work, that you would like to do it in the background as you think it would be a nice thing if the gui would stay responsive to the user actions. Sometimes invoking a NSThread is a valid solution, but if you want to schedule many tiny tasks to the background creating a new thread for each of them might not be the best idea.

Another possibility is using Distributed Objects (DO). But DO for intra-application communication might be a bit of an overkill and during my experimenting I experienced, that outsourcing a working thread via DO and using the NSRunLoop is not the ideal solution: if the runloop of the worker thread is currently busy working on a past message you have to wait on the main thread if you want to post a new DO message until the worker thread's runloop is free again.

So a solution I am using for the background Spotlight export of my app is based on the producers and consumer model: I create a thread safe queue in which the producer (main) thread posts new tasks and a consumer thread performs these tasks. This is very lightweight and the main thread does not block during the posting of the tasks.

How does it work? Again using NSInvocations. The tasks, that are written to the queue are just NSInvocations, so you can post any message the target object understands.

So here's an example:

In the main thread you initialize NBInvocationQueue and start the worker thread


NBInvocationQueue *finderEventQueue = [[NBInvocationQueue alloc] init];
[NSThread detachNewThreadSelector:@selector(runQueueThread) toTarget:finderEventQueue withObject:nil];


Now the NBInvocationQueue is ready to take messages. You post them like this:


[[finderEventQueue performThreadedWithTarget:spotlightExporter]
exportFinderCommentAtPath:path
tags:tagArray
comment:comment]

This will invoke [spotlightExporter exportFinderCommentAtPath:...] on the worker thread.

The methods to queue messages are:

-(id)performThreadedWithTarget:(id)target;
-(id)performThreadedWithTarget:(id)target afterDelay:(NSTimeInterval)delay;


If you want to stop the worker thread you can use the method:

-(void)stopThreadWaitUntil:(BOOL)wait;

which waits until all open tasks have been de-queued if the flag "wait" is set.

The source code is here:

NBInvocationQueue.h and NBInvocationQueue.m

Feel free to use this class as you like, NO WARRANTY!

Inspiration for this class has been producers and consumer model and CInvocationGrabber from ToxicSoftware.

Labels: , ,

Tuesday, December 12, 2006

NSInvocation cleans code

Update: (2008-08-07)
There's been a small (but potent) bug in the given implementation. I somehow move the release of the invocation:
[myInvocation release]; myInvocation = nil;
into the dealloc method of the _UFLatePerformer class. This is bollocks, because the invocation contains _UFLatePerformer as target itself. Therefore I have to release the invocation as soon as it has been invoked. Otherwise there is a retain-cycle and the dealloc-method of _UFLatePerformer is never called.
The code below has been updated accordingly.


During the development of Nifty Box I've grown to use NSInvocations more and more. One thing I am using this class especially for is a wrapper for NSObject's performSelector:withObject:afterDelay:. This method invokes the selector with one argument after a specified delay. My problem was that I can only pass one argument for the selector, but it often happens that I want to invoke a method with more than one argument. So I had to pack all the arguments into a NSDictionary, write a proxy method, inside this proxy function unpack the arguments from the dictionary and invoke the original method. This is messy.

For this I have written a small category add-on for NSObject:

@interface _UFLatePerformer : NSObject {
NSInvocation *myInvocation;
id target;
}
-(id)initWithTarget:(id)theTarget;
-(void)performLate;
@end

@interface NSObject (NSObject_laterInvocation)
-(id)performAfterDelay:(NSTimeInterval)delay;
@end

@implementation _UFLatePerformer

- (id) initWithTarget:(id)theTarget {
[super init];
if (self != nil) {
target = theTarget;
[target retain];
}
return self;
}

- (void) dealloc {
[target release]; target = nil;

[super dealloc];
}


-(void)forwardInvocation:(NSInvocation *)invocation;
{
myInvocation = invocation;
[myInvocation retain];
[myInvocation retainArguments];
}

-(BOOL)respondsToSelector:(SEL)aSelector;
{
BOOL result = [super respondsToSelector:aSelector];
if (result == NO)
result = [target respondsToSelector:aSelector];
return result;
}

-(NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector;
{
NSMethodSignature *result = [super methodSignatureForSelector:aSelector];
if (!result)
result = [target methodSignatureForSelector:aSelector];
return result;
}


-(void)performLate;
{
[myInvocation invokeWithTarget:target];
[myInvocation release]; myInvocation = nil;
}

@end


@implementation NSObject (NSObject_laterInvocation)

-(id)performAfterDelay:(NSTimeInterval)delay;
{
_UFLatePerformer *latePerformer = [[_UFLatePerformer alloc]
initWithTarget:self];
[latePerformer performSelector:@selector(performLate)
withObject:nil afterDelay:delay];
[latePerformer release];
return latePerformer;
}

@end


This basically works like the NSUndoManager's [[undoManager prepareWithInvocationTarget:self] myMethod:someArgument anotherArgument:secondArgument]

You use this new method like that:

[[myObject performAfterDelay:0.0] someMehodWithArgument1:argument1 argument2:argument2]


This is an implementation of the Trampoline Object.

Feel free to use this snippet as you wish. No warranty however.

Labels: , ,