Home > Threading > Part I: Spawning Threads Using Selectors With Multiple Parameters

Part I: Spawning Threads Using Selectors With Multiple Parameters

If you’ve spent much time working with thread in Cocoa, you have likely come across the following roadblock: you can only specify one parameter when spawning a new thread from a selector.  The first reaction is to rewrite your method with a single parameter that takes collection of objects and then unpacks them.  This can lead to untidy or prolix code, though, and doesn’t do anything for readability or debug-ability. You also need to know when you define your method that you will want to spawn it off–otherwise you face refactoring down the road.  At any rate, spawning via selector is a convenience method; we shouldn’t be forced to change our code in order to use it.

You can see glimmers of hope, though, as you study the problem.  Objective-C allows you to include multiple parameters in a selector, as in @selector(myMethod:withAValue:andAnother:). However, you will throw an exception at runtime since you can only specify one context object, and thus, the runtime will only have values to pass for one of the arguments. So close, and yet, so far away.  Is our only other option subclassing NSThread?  Thankfully, no.  We can have our cake and eat it, too, with just a little work on our part. The answer lies in the NSInvocation class.
 Huh? Invocation? The Apple developer class reference explains the idea succinctly:

An NSInvocation is an Objective-C message rendered static, that is, it is an action turned into an object … An NSInvocation object contains all the elements of an Objective-C message: a target, a selector, arguments, and the return value. Each of these elements can be set directly, and the return value is set automatically when the NSInvocation object is dispatched.

Oooh.  Clever.  Now the class name makes more sense. Instead of invoking of an object’s method using source code (at design time), we take a different approach.  We use an object to invoke another object’s method (at runtime).  Here, Objective-C shows us just how powerful introspection can be. Now that we have NSInvocation, how do we get back to spawning off arbitrary methods with multiple parameters? We create our own convenience method.  Let’s look at an example:

/** Calls arbitary methods inside of a new thread. */
- (void)performSelector:(SEL)aSelector withContext:(id)context
{
	// Before we get ahead of ourselves, let's do some
	// sanity checking on the context object pass
	// to the method.
	NSInvocationOperation *theOperation ;
	if ([context isKindOfClass:[NSArray class]])
	{
		// The docs explicity tell you not to use alloc/init,
		// but instead, use the class method invocationWithMethodSignature,
		// which takes a selector and returns its method's signature.
		NSMethodSignature *theSignature =
			[self methodSignatureForSelector:aSelector];

		// With our signature in hand, we can now call our
		// class method, which returns a usable (and autoreleased)
		// NSInvocation instance.
		NSInvocation *theInvocation =
			[NSInvocation invocationWithMethodSignature:theSignature];

		// Now for the real automagic fun!  We will loop through our context array,
		// and match up each parameter with the corresponding value
		// passed to us in our array.  First we will check to see if the there
		// are the same number of parameters and argument values (always nice).
		// Since we know context is a kind of NSArray, we cast it as one.
		NSArray *parameterValues = (NSArray *)context;
		NSUInteger parameterCount = [parameterValues count];

		// Objective-C actually converts selectors into C methods, and
		// some of that behind-the-scenes chicanary will affect us here.
		// To get an idea of how, let's look at the official guidance:

	      /* From SDK Docs:

		 NSInvocation Class Reference ...
		 There are always at least 2 arguments, because an NSMethodSignature
		 object includes the hidden arguments self and _cmd, which are the
		 first two arguments passed to every method implementation.

		 NSMethodSignature Class Reference ...
		 Indices 0 and 1 indicate the hidden arguments self and _cmd,
		 respectively; you should set these values directly with the
		 setTarget: and setSelector: methods. Use indices 2 and greater
		 for the arguments normally passed in a message.
		 */

		NSUInteger argumentCount = [theSignature numberOfArguments] - 2;

		if (parameterCount == argumentCount)
		{
			[theInvocation setTarget:self];		 // There's our index 0.
			[theInvocation setSelector:theSelector]; // There's our index 1.

			// Now for arguments 2 and up...
			NSUInteger i, count = parameterCount;

			for (i = 0; i < count; i++) {

				// We're agnostic about object types, so assume nothing.
				id currentValue = [parameterValues objectAtIndex:i];

				// Well, not entirely agnostic. We do have one problem
				// case that is likely to occur somewhat frequently.
				//
				// Objective-C data types, like int, float, etc., are
				// not objects.  We can't pass them in an array. However,
				// there are many methods that take these types of
				// parameters.  Since we have a goal of supporting
				// arbitrary methods--without modifying them--we need
				// some way to transport these.  A quick and simple way
				// so to wrap them in an NSValue object, then add them
				// to our array.
				//
				// Good so now we have our NSValue.  Now what? Well,
				// now we need to unwrap the basic value nestled inside.
				// Once tranformed, we can pass it as a parameter.
				// Here's how we do that:
				//
				// Test to see if we have a foundation class
				// wrapped in an NSValue.
				if ([currentValue isKindOfClass:[NSValue class]])
				{
					void *bufferForValue;
					[currentValue getValue:&bufferForValue];
					[theInvocation
						setArgument:&bufferForValue
						atIndex:(i + 2)];
					// The +2 represents the (self) and (cmd) offsets
				} else
				{
					[theInvocation
						setArgument:&currentValue
						atIndex:(i + 2)];
					// Again, our +2 represents (self) and (cmd) offsets
				}
			}
			// That's it! (For our NSInvocation).
		}
		// Now we use the invocation to create our operation, which I explain
		// a bit more about below.  I'm including it as reference, since
		// there are good advantages to using a queue.
		theOperation = [[NSInvocationOperation alloc]
					initWithInvocation:theInvocation];
	}
	else
	{	// Okay. We were not passed an array in our context parameter.
		// Well, that's just fine.  If we received something, we'll just
		// pass it on.  This way, our users don't have to go through the
		// hassle of creating an NSArray of just one item.  We could check
		// to see if our selector has just one parameter, and then ensure
		// that context is not nil, but that's left as an exercise to
		// the reader.
		// Note, context is of type (void), so it doesn't need to be
		// wrapped/unwrapped in an NSValue object.  Woo hoo!
		// Invoke a selector with a single parameter.
		theOperation = [[NSInvocationOperation alloc]
					initWithTarget:self
					selector:NSSelectorFromString(aSelector)
					object:context];
	}
	// In this example, I'm submitting my NSInvocationOperation
	// to an NSOperationQueue, which I have already built using
	// a singleton pattern.  You could also use NSThread, but
	// there are some pluses to using NSInvocationQueues for my
	// application (and probably yours too). For NSThread, you
	// use NSInvocation instead of NSInvocationOperation, and
	// might end up with something like:

	// [theInvocation performSelectorInBackground:@selector(invoke) withObject:nil]

	// where the invoke selector is sent to theInvocation, telling it to
	// call the method with the arguements as we defined above, but on
	// a background thread.  The other performSelector* methods work similarly.
	//
	// In this case, though, I'm adding my operation to the queue.
	// Add the operation to the internal operation queue
	// managed by our application delegate.
	[[myAppDelegate sharedOperationQueue] addOperation:theOperation] ;

	// Make sure theOperation gets released later, once it's it finished running.
	[theOperation release];
}

To invoke our, er, invocation, we just send a message like:

[self performSelector:@selector(myMethodWithA:andB:andC:)
        withContext:myArrayOfObjects];

Pretty convenient, and very flexible. When using the built-in convenience methods that Apple provides, the object that you pass to the withObject: parameter is retained automatically. This is not the case, by default, when using NSInvocation. If your code relies on that auto-retain, not to worry. All you need to do is add [theInvocation retainArguments] before you invoke (say at line 21 above).

Let’s take a breath. We covered a lot of ground…but not all of it. When you’re ready, test your endurance with Part II of this article, where we streamline our new code.

About these ads
  1. 02/12/2013 at 7:12 PM | #1

    what about performing the selector after a delay??

    • Zack
      02/26/2013 at 10:31 AM | #2

      Probably the easiest way is to add afterDelay:[NSTimeInterval] to the end of line 130.

  1. 05/18/2010 at 12:38 PM | #1

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: