June 4, 2009

Multiple Parameters to Threads

This hint is fairly trivial, but I was surprised I could not find any information on this little "problem". I wanted to pass more than one parameter to a thread entry method. The standard NSThread class only supports 0 or 1 parameter to be passed, as can be seen in this initializer:

- (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument

Another way of starting a thread is with the method:

+ (void)detachNewThreadSelector:(SEL)aSelector toTarget:(id)aTarget withObject:(id)anArgument

But as you can see, this has the same limitation. The object parameter can be nil or some other parameter, but at most 1 parameter can be passed. Even the more modern NSOperation class has this same limitation and its remedy is pretty much the same as described here. There are two ways I see to overcome this limitation. One is more complicated than the other, but I will present both.  

Subclassing NSThread

The more complicated way is to subclass the NSThread class and override the main method to be your thread entry point. Since you create your own class, your initializer can accept any number of parameters, which you assign to instance variables. Then the main method can easily access this data:


@interface MyThread : NSThread
{
  NSString* data1;
  NSInteger data2;
}

- (id) initWithData1:(NSString*) data1 andData2:(NSInteger) data2;
- (void) main;

@end

@implementation MyThread

- (id) initWithData1:(NSString*) data1 andData2:(NSInteger) data2
{
  if((self = [super init])) {
    // save off our parameters
    self.data1 = data1;
    [self.data1 retain];

    self.data2 = data2;
  }

  return self;
}

- (void) main
{
  // no need to call [super main], as per the Apple reference docs
  // do whatever your thread needs to do
}

- (void) dealloc
{
  [data1 release];
}

@end


In order to kick off this thread, you do something like this:


// initialize it, passing all your parameters
MyThread* thread = [[MyThread alloc] initWithData1:@"hello there" andData2:23];

// now start it
[thread start];

// done with it
[thread release];


As you can see, this can be a bit involved, forcing you to create a new class, but it gets the job done.  

Stuffing Parameters

The other way is simpler, but at the same time a little clunkier. This way uses the simple initializer or the detach method as mentioned above. The way to get multiple parameters passed is to cheat: we pass a single container that holds all our parameters. Since this parameter is defined as id, it can be anything (as long as it is an object and not a primitive type). The perfect container is an NSArray. However, this is where things get a little clunky. First you have to create the array and populate it with your data. Worse is the fact that you can only put class objects into the array; scalars like integers will have to be converted to an object before adding.


// fire up a thread
[NSThread detachNewThreadSelector:@selector(myMain:)
                      toTarget:self
                    withObject:[NSArray arrayWithObjects:@"hello there",
                                        [NSNumber numberWithInt:23], nil]];


And then your thread entry method must pull the parameters back out of the array:


- (void) myMain:(NSArray*) parameters
{
 NSString* data1 = [parameters objectAtIndex:0];
 NSInteger data2 = [(NSNumber*) [parameters objectAtIndex:1] intValue];

 // the rest ...
}


Neither of these two solutions are perfect. The best solution, of course, is to write your thread code such that it doesn't need more than one parameter, but sometimes this is unavoidable. If any readers out there have any other ideas to solve this, I would love to hear them!

2 comments:

  1. You were actually very close in your April post, "Invoking Selectors".

    I have written up a post with code that goes into detail.

    ReplyDelete
  2. Ah, what a great idea! Why didn't I put two and two together? Thanks for the great tip.

    ReplyDelete